ima2-gen 2.0.1 → 2.0.3

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 (100) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +10 -1
  3. package/bin/commands/backfillThumbs.js +6 -0
  4. package/bin/commands/gen.js +13 -5
  5. package/bin/commands/multimode.js +7 -5
  6. package/bin/commands/node.js +2 -1
  7. package/bin/ima2.js +14 -10
  8. package/config.js +8 -1
  9. package/docs/API.md +133 -9
  10. package/docs/CLI.md +6 -5
  11. package/docs/FAQ.ko.md +16 -0
  12. package/docs/FAQ.md +30 -0
  13. package/docs/README.ko.md +7 -3
  14. package/docs/migration/runtime-test-inventory.md +25 -1
  15. package/lib/agentCommandParser.js +10 -6
  16. package/lib/agentGenerationPlanner.js +96 -19
  17. package/lib/agentImageVideoGen.js +281 -0
  18. package/lib/agentPlannerModel.js +172 -0
  19. package/lib/agentQuestionResponder.js +9 -3
  20. package/lib/agentQueueStore.js +42 -0
  21. package/lib/agentQueueWorker.js +57 -7
  22. package/lib/agentRuntime.js +103 -274
  23. package/lib/agentSettings.js +9 -6
  24. package/lib/agentToolManifest.js +90 -0
  25. package/lib/agentTypes.js +1 -0
  26. package/lib/agyCli.js +31 -0
  27. package/lib/agyImageAdapter.js +104 -12
  28. package/lib/capabilities.js +7 -5
  29. package/lib/configKeys.js +6 -0
  30. package/lib/errorClassify.js +8 -7
  31. package/lib/eventBus.js +71 -0
  32. package/lib/geminiApiImageAdapter.js +24 -21
  33. package/lib/generationErrors.js +3 -1
  34. package/lib/generationRequestLog.js +25 -0
  35. package/lib/grokImageAdapter.js +76 -144
  36. package/lib/grokImageCore.js +153 -0
  37. package/lib/grokMultimodeAdapter.js +7 -4
  38. package/lib/grokVideoAdapter.js +3 -2
  39. package/lib/grokVideoCanvas.js +13 -0
  40. package/lib/grokVideoPlannerPrompt.js +52 -12
  41. package/lib/historyList.js +1 -0
  42. package/lib/inflight.js +54 -17
  43. package/lib/multimodeHelpers.js +11 -0
  44. package/lib/nodeHelpers.js +59 -0
  45. package/lib/oauthProxy/generators.js +6 -5
  46. package/lib/oauthProxy/prompts.js +60 -41
  47. package/lib/oauthProxy/streams.js +1 -1
  48. package/lib/promptBuilder/systemPrompt.js +2 -5
  49. package/lib/promptSafetyPolicy.js +1 -5
  50. package/lib/responsesDoctor.js +3 -3
  51. package/lib/responsesFallback.js +29 -15
  52. package/lib/responsesImageAdapter.js +11 -7
  53. package/lib/routeHelpers.js +44 -0
  54. package/lib/ssePublish.js +12 -0
  55. package/lib/storyboardPrefix.js +28 -0
  56. package/lib/thumbBackfill.js +16 -5
  57. package/node_modules/qs/CHANGELOG.md +10 -0
  58. package/node_modules/qs/README.md +1 -1
  59. package/node_modules/qs/dist/qs.js +15 -15
  60. package/node_modules/qs/eslint.config.mjs +1 -0
  61. package/node_modules/qs/lib/parse.js +52 -22
  62. package/node_modules/qs/lib/stringify.js +11 -4
  63. package/node_modules/qs/package.json +2 -2
  64. package/node_modules/qs/test/parse.js +49 -0
  65. package/node_modules/qs/test/stringify.js +129 -0
  66. package/package.json +4 -1
  67. package/routes/agent.js +19 -2
  68. package/routes/agy.js +48 -0
  69. package/routes/auth.js +6 -2
  70. package/routes/edit.js +7 -1
  71. package/routes/events.js +78 -0
  72. package/routes/generate.js +111 -128
  73. package/routes/generationRequestLog.js +16 -0
  74. package/routes/index.js +6 -0
  75. package/routes/keys.js +22 -2
  76. package/routes/multimode.js +100 -57
  77. package/routes/nodes.js +64 -108
  78. package/routes/video.js +100 -17
  79. package/skills/ima2/SKILL.md +98 -21
  80. package/ui/dist/.vite/manifest.json +22 -12
  81. package/ui/dist/assets/AgentWorkspace-qEMrATBP.js +3 -0
  82. package/ui/dist/assets/{CardNewsWorkspace-Dqyc1WZ1.js → CardNewsWorkspace-DoJnVHmn.js} +1 -1
  83. package/ui/dist/assets/GenerationRequestLogPanel-BDWRRLNV.js +1 -0
  84. package/ui/dist/assets/{NodeCanvas-ChEXzQbb.js → NodeCanvas-DIMo45sp.js} +3 -3
  85. package/ui/dist/assets/{PromptBuilderPanel-B95ZufnR.js → PromptBuilderPanel-BF3lcSJD.js} +2 -2
  86. package/ui/dist/assets/{PromptImportDialog-DGOwFQET.js → PromptImportDialog-Cy6ZymKG.js} +2 -2
  87. package/ui/dist/assets/{PromptImportDiscoverySection-CgvdnR49.js → PromptImportDiscoverySection-CzuW-8P2.js} +1 -1
  88. package/ui/dist/assets/PromptImportFolderSection-DP5ywnsv.js +1 -0
  89. package/ui/dist/assets/{PromptLibraryPanel-B9kndPw1.js → PromptLibraryPanel-CAsKr7CV.js} +2 -2
  90. package/ui/dist/assets/SettingsWorkspace-_PdPVsxi.js +1 -0
  91. package/ui/dist/assets/index-BUinlX2n.js +4 -0
  92. package/ui/dist/assets/index-CX3fge8X.css +1 -0
  93. package/ui/dist/assets/index-ygo6nfqx.js +23 -0
  94. package/ui/dist/index.html +2 -2
  95. package/ui/dist/assets/AgentWorkspace-CYv84Rus.js +0 -3
  96. package/ui/dist/assets/PromptImportFolderSection-CfUye9J8.js +0 -1
  97. package/ui/dist/assets/SettingsWorkspace-B3tgLrmF.js +0 -1
  98. package/ui/dist/assets/index-BhcvL0g-.js +0 -1
  99. package/ui/dist/assets/index-BtK3YhJc.js +0 -39
  100. package/ui/dist/assets/index-ClOLOjnA.css +0 -1
package/docs/API.md CHANGED
@@ -10,14 +10,14 @@ http://localhost:3333
10
10
 
11
11
  ## Provider Policy
12
12
 
13
- Image generation supports OAuth, API-key, Grok, and Gemini (agy) providers.
13
+ Image generation supports OAuth, API-key, Grok, and Gemini (`agy` and `gemini-api`) providers.
14
14
 
15
15
  - `provider: "oauth"` uses the local Codex OAuth proxy.
16
16
  - `provider: "api"` uses the OpenAI Responses API with the hosted `image_generation` tool.
17
17
  - `provider: "grok"` uses the bundled progrok xAI proxy. Classic, Node, and Agent generation run mandatory xAI Web Search through `/v1/responses`, then run a `grok-4.3` planner call with a forced local `generate_image` function, then ima2 executes xAI `/v1/images/generations`. If reference images, a Node parent image, or an Agent current image are attached, the final step switches to xAI `/v1/images/edits` so image-to-image context is preserved.
18
18
  - `provider: "agy"` spawns the Antigravity CLI (`agy -p`) to generate images via Google Gemini's `default_api:generate_image` tool. Model is `nano-banana-2`. Output is fixed at 1024×1024 JPEG. Max 3 reference images (i2i). No web search, quality, size, or mask controls. Multimode returns a single image. Video is unsupported (`AGY_VIDEO_UNSUPPORTED`).
19
19
  - `provider: "grok-api"` uses a direct xAI API key instead of the bundled progrok OAuth proxy. Same pipeline as `grok` (Web Search → planner → `/v1/images/generations`), same aspect ratio and resolution options. Requires an xAI API key configured via the web UI key management or `XAI_API_KEY` env var. Also supports video generation.
20
- - `provider: "gemini-api"` calls the Google Generative Language API directly (or Vertex AI with a service account JSON). Supports models `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). Supports variable aspect ratios and resolutions (512px4K). Requires a `GEMINI_API_KEY` env var, web UI key management, or a Vertex AI service account JSON. No web search or mask controls.
20
+ - `provider: "gemini-api"` calls the Google Generative Language API directly (or Vertex AI with a service account JSON). Supports models `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). Supports variable aspect ratios (1:1 through 21:9) and four resolution tiers (512px, 1K, 2K, 4K) on both auth paths — the direct API path sends `generation_config.response_format.image` (snake_case) while the Vertex AI endpoint (`aiplatform.googleapis.com`) sends `generationConfig.imageConfig` (camelCase). With `size: "auto"` the image config is omitted entirely and the model decides ratio/size. Auth: `GEMINI_API_KEY` env var, web UI key management (`/api/keys/gemini`), or a Vertex AI service account JSON (`VERTEX_SERVICE_ACCOUNT_JSON` or `/api/keys/vertex`). When both Vertex credentials and an API key are configured, Vertex takes priority. The chosen auth mode (`apikey` or `vertex`) persists to `~/.ima2/config.json` as `geminiAuthMode` and is restored on server startup. Per-model cost: `nano-banana-2` (Flash): 512=$0.001, 1K=$0.003, 2K=$0.004, 4K=$0.006; `nano-banana-pro`: 1K=$0.007, 2K=$0.007, 4K=$0.013. No web search or mask controls.
21
21
  - API-key generation covers classic generate, edit, mask-guided edit, multimode, and node generation.
22
22
  - If `provider: "api"` is requested without an API key, routes fail before upstream with `401` and `API_KEY_REQUIRED`.
23
23
  - Grok generation maps `size` to xAI `aspect_ratio` and `resolution`; it does not send an OpenAI-style `size` field upstream. Grok edit uses xAI `/v1/images/edits`; Grok mask edit remains unsupported and returns `GROK_MASK_UNSUPPORTED`.
@@ -35,6 +35,16 @@ Generation section below for the full endpoint specification.
35
35
  | `GET` | `/api/oauth/status` | OAuth proxy status and visible models |
36
36
  | `GET` | `/api/grok/status` | Bundled progrok status and visible xAI image models |
37
37
  | `GET` | `/api/billing` | Billing/status probe, including API key source when configured |
38
+ | `GET` | `/api/quota` | Provider quota: returns `{ codex, grok }`. Grok result includes `billing: { usedUsd, limitUsd }` and a `monthly` percent window drawn from the xAI billing API. |
39
+
40
+ ## Account Switching
41
+
42
+ | Method | Path | Notes |
43
+ |---|---|---|
44
+ | `POST` | `/api/auth/switch` | Start a device-code OAuth flow. Body: `{ "provider": "grok" \| "codex" }`. Returns `{ sessionId, userCode, verificationUrl }`. |
45
+ | `GET` | `/api/auth/switch/:sessionId` | Poll switch-account session status. Returns `{ status }` where status is `pending`, `complete`, `error`, or `expired`. |
46
+
47
+ The Switch Account flow opens a browser verification URL. Once the user completes the device-code step, the server saves the new credentials (Grok: `~/.progrok/auth.json`; Codex: via `codex login --device-auth`) and the session transitions to `complete`. This endpoint is surfaced as a **Switch Account** button in the Settings QuotaCard for Grok and Codex providers.
38
48
 
39
49
  ## Storage
40
50
 
@@ -84,9 +94,81 @@ Storage `state` values:
84
94
  | `GET` | `/api/inflight` | Active jobs only by default |
85
95
  | `GET` | `/api/inflight?includeTerminal=1` | Includes recent terminal jobs for debugging |
86
96
  | `DELETE` | `/api/inflight/:requestId` | Cancel or forget an active job |
97
+ | `GET` | `/api/events` | Persistent SSE multiplex channel for all async generation progress (see below) |
87
98
 
88
99
  In-flight logs and responses use `requestId` for correlation. Logs should not include raw prompts, reference data URLs, generated base64, tokens, cookies, auth headers, or raw upstream bodies.
89
100
 
101
+ ## Events (SSE Multiplexing)
102
+
103
+ ### `GET /api/events` (SSE Multiplexing)
104
+
105
+ Single persistent Server-Sent Events channel that carries progress for all async generation jobs. The browser UI opens one `EventSource` here instead of holding a per-request SSE connection for each job, avoiding browser per-origin connection limits.
106
+
107
+ | Query | Notes |
108
+ |---|---|
109
+ | `lastEventId` | Optional. Reconnect cursor; also accepted via the `Last-Event-ID` request header |
110
+
111
+ **Response**: `text/event-stream` (persistent). Each frame uses standard SSE fields `id`, `event`, and `data` (JSON).
112
+
113
+ **Connection limits**: When active listeners reach 512, the server returns `503` with `SSE_CAPACITY` before opening the stream.
114
+
115
+ **Heartbeat**: Every 15 seconds the server writes a comment frame:
116
+
117
+ ```text
118
+ : ping
119
+ ```
120
+
121
+ **Replay**: On reconnect, the server replays events from an in-memory ring buffer (size 2000) for IDs newer than `lastEventId`. Large image payloads (>1000 characters) are omitted from replay with `_imageOmitted: true` in the `data` payload. If the requested ID is older than the oldest buffered event, the server emits a `replay-gap` event before live fan-out:
122
+
123
+ | Event | Data | Description |
124
+ |---|---|---|
125
+ | `replay-gap` | `{ lastEventId, oldestAvailableId }` | Client should reconcile inflight state (for example via `GET /api/inflight`) |
126
+
127
+ **Job routing**: Every `data` payload includes `jobId` (same value as the job's `requestId`). Event bodies also carry `requestId` where applicable. Clients filter events by matching `data.jobId` or `data.requestId` to the job they started.
128
+
129
+ **Event types** (fan-out to all connected clients):
130
+
131
+ | Event | Emitted by | Description |
132
+ |---|---|---|
133
+ | `phase` | node, multimode, video | Lifecycle phase change |
134
+ | `partial` | node, multimode | Progressive preview image (base64 data URL) |
135
+ | `image` | multimode | Final saved `GenerateItem` for one sequence image |
136
+ | `done` | node, multimode, video | Terminal success payload (route-specific shape) |
137
+ | `error` | all generation routes | Terminal failure |
138
+ | `submitted` | video | Job submitted to xAI |
139
+ | `progress` | video | Progress fraction 0.0–1.0 |
140
+ | `planning` | video | Video planner running |
141
+
142
+ Example SSE frame:
143
+
144
+ ```text
145
+ id: 42
146
+ event: phase
147
+ data: {"requestId":"req_abc","jobId":"req_abc","phase":"streaming"}
148
+ ```
149
+
150
+ ### Async generation mode
151
+
152
+ `POST /api/node/generate`, `POST /api/generate/multimode`, and `POST /api/video/generate` support an async POST mode for clients that already hold `GET /api/events`:
153
+
154
+ ```json
155
+ {
156
+ "async": true,
157
+ "requestId": "req_xxx",
158
+ "...": "other route fields"
159
+ }
160
+ ```
161
+
162
+ | Outcome | HTTP | Body |
163
+ |---|---|---|
164
+ | Accepted | `202` | `{ "requestId": "req_xxx" }` |
165
+ | Duplicate active `requestId` | `409` | `REQUEST_ID_IN_USE` |
166
+ | More than the configured concurrent active job limit | `429` | `TOO_MANY_JOBS` with `Retry-After: 5`; default limit is `24` via `IMA2_MAX_PARALLEL` |
167
+
168
+ Progress events are published on `GET /api/events`. The POST response returns immediately; clients must not expect SSE on the POST connection when `async: true`.
169
+
170
+ CLI and legacy clients omit `async` and keep the original behavior: per-request SSE on the same POST response (`Accept: text/event-stream` where applicable). The server dual-emits in that mode — it writes SSE to the POST response and also publishes the same events on `GET /api/events`.
171
+
90
172
  ## Generation
91
173
 
92
174
  ### `POST /api/generate`
@@ -195,14 +277,46 @@ Body fields:
195
277
  }
196
278
  ```
197
279
 
198
- When `parentNodeId` is present, the server loads the stored parent node image and uses the edit path. Extra node references are currently supported only for root nodes.
280
+ When `parentNodeId` is present, the server loads the stored parent node image and uses the edit path. Node-local references are allowed on both root and child/edit nodes; for child/edit nodes the parent image is sent first, then references, then the text prompt.
199
281
 
200
282
  With `provider: "grok"`, Node Mode uses the same xAI search + `grok-4.3` planner + Images API pipeline as classic generation. A parent node image, `externalSrc`, or extra references are passed to the planner and then to xAI `/v1/images/edits`; otherwise the final call uses `/v1/images/generations`. Grok Node requests are capped at three total input images, counting the parent/current image plus references, and return `GROK_REF_TOO_MANY` before upstream when that limit is exceeded. `quality: "high"` promotes the final image model to `grok-imagine-image-quality`.
201
283
 
202
- The route can stream Server-Sent Events when the client sends `Accept: text/event-stream`. Possible events include `phase`, `partial`, `done`, and `error`.
284
+ The route can stream Server-Sent Events when the client sends `Accept: text/event-stream`. Possible events include `phase`, `partial`, `done`, and `error`. Alternatively, send `{ "async": true, "requestId": "req_xxx" }` in the body to receive `202 { requestId }` immediately and follow progress on `GET /api/events` (see Events section).
203
285
 
204
286
  Grok Node SSE responses do not include Responses API `partial` image events because the xAI Images API call is synchronous JSON. They still emit `phase` and `done`/`error` events so the Node UI can use the same in-flight lifecycle.
205
287
 
288
+ ### `POST /api/generate/multimode` (SSE)
289
+
290
+ Multi-image sequence generation. SSE-only on the POST response unless async mode is used.
291
+
292
+ ```json
293
+ {
294
+ "prompt": "a story in four panels",
295
+ "maxImages": 4,
296
+ "quality": "medium",
297
+ "size": "1024x1024",
298
+ "format": "png",
299
+ "moderation": "low",
300
+ "model": "gpt-5.4",
301
+ "provider": "oauth",
302
+ "references": [],
303
+ "requestId": "optional-client-id",
304
+ "async": false
305
+ }
306
+ ```
307
+
308
+ Send `Accept: text/event-stream` for per-request SSE on the POST connection. Or set `"async": true` with a client `requestId` to get `202 { requestId }` and receive events on `GET /api/events`.
309
+
310
+ **SSE events**:
311
+
312
+ | Event | Data | Description |
313
+ |---|---|---|
314
+ | `phase` | `{ requestId, phase, sequenceId?, maxImages? }` | Lifecycle phase |
315
+ | `partial` | `{ requestId, image, index }` | Progressive preview |
316
+ | `image` | full `GenerateItem` | One saved sequence image |
317
+ | `done` | route-specific summary; may include `status: "partial"` after timeout if at least one image was saved | Sequence complete |
318
+ | `error` | `{ requestId, error, code?, status? }` | Generation failed |
319
+
206
320
  ### `GET /api/node/:nodeId`
207
321
 
208
322
  Fetch stored node metadata and asset URL.
@@ -228,7 +342,7 @@ Server-side validation may return these reference codes:
228
342
 
229
343
  ### `POST /api/video/generate` (SSE)
230
344
 
231
- Generate a video via the Grok video provider. Returns Server-Sent Events.
345
+ Generate a video via the Grok video provider. Returns Server-Sent Events on the POST connection, or accepts async mode (`{ "async": true, "requestId": "req_xxx" }`) for `202 { requestId }` with progress on `GET /api/events` (see Events section).
232
346
 
233
347
  ```json
234
348
  {
@@ -263,7 +377,7 @@ Generate a video via the Grok video provider. Returns Server-Sent Events.
263
377
  | Field | Type | Default | Notes |
264
378
  |---|---|---|---|
265
379
  | `prompt` | string | — | Required |
266
- | `provider` | string | `"grok"` | Must be `"grok"` |
380
+ | `provider` | string | `"grok"` | `"grok"` or `"grok-api"` |
267
381
  | `model` | string | `grok-imagine-video` | Video model |
268
382
  | `duration` | integer | `5` | 1–15 seconds (clamped to 10 for reference-to-video) |
269
383
  | `resolution` | string | `"480p"` | `480p` or `720p` |
@@ -488,6 +602,9 @@ Style-sheet extraction can require an API key/openai client. Image generation al
488
602
  | `GEMINI_API_SAFETY_BLOCKED` | Gemini API generation blocked by safety filter |
489
603
  | `GEMINI_API_NO_IMAGE` | Gemini API returned no image in response |
490
604
  | `VIDEO_PROVIDER_UNSUPPORTED` | Video generation requires provider `"grok"` or `"grok-api"` |
605
+ | `SSE_CAPACITY` | More than 512 concurrent `GET /api/events` listeners |
606
+ | `REQUEST_ID_IN_USE` | Async POST used a `requestId` that already has an active job |
607
+ | `TOO_MANY_JOBS` | More than the configured concurrent active generation job limit (`Retry-After: 5`; default `24`) |
491
608
 
492
609
  ## Key Management
493
610
 
@@ -495,11 +612,12 @@ API key management endpoints for configuring provider credentials at runtime thr
495
612
 
496
613
  | Endpoint | Method | Description |
497
614
  |---|---|---|
498
- | `/api/keys/status` | GET | Returns configured/valid/maskedKey status for all providers (openai, xai, gemini, vertex) |
615
+ | `/api/keys/status` | GET | Returns configured/valid/maskedKey status for all providers (openai, xai, gemini, vertex) plus `geminiAuthMode` (`"apikey"` or `"vertex"`) |
499
616
  | `/api/keys/:provider` | PUT | Save an API key. Body: `{ "apiKey": "..." }`. Validates key format and upstream before saving to config.json. Provider: `openai`, `xai`, or `gemini`. |
500
617
  | `/api/keys/:provider` | DELETE | Remove a config-sourced API key. Env-sourced keys cannot be removed (`ENV_KEY_IMMUTABLE`). |
501
618
  | `/api/keys/vertex` | PUT | Save a Vertex AI service account JSON. Body: `{ "serviceAccountJson": "..." }`. Validates JSON structure (`type: "service_account"`, `project_id` required). |
502
619
  | `/api/keys/vertex` | DELETE | Remove a config-sourced Vertex AI service account. |
620
+ | `/api/keys/gemini-auth-mode` | PUT | Persist the Gemini auth mode chosen in the settings dropdown. Body: `{ "mode": "apikey" \| "vertex" }`. Saved to `config.json` and hot-updated. |
503
621
 
504
622
  Keys saved via PUT are stored in `config.json` and hot-updated in the runtime context (no server restart required). Keys loaded from environment variables (`OPENAI_API_KEY`, `XAI_API_KEY`, `GEMINI_API_KEY`, `VERTEX_SERVICE_ACCOUNT_JSON`) take precedence and are immutable through the API.
505
623
 
@@ -529,7 +647,7 @@ Most server routes under `/api/*` have a CLI wrapper. The exception is **Agent M
529
647
  | `POST /api/node/generate` (SSE) / `GET /api/node/:id` | `ima2 node generate` / `ima2 node show` |
530
648
  | `GET /api/history` | `ima2 ls` |
531
649
  | `DELETE /api/history/:name` / `…/permanent` | `ima2 history rm [--permanent]` |
532
- | `POST /api/history/restore` | `ima2 history restore --trash-id` |
650
+ | `POST /api/history/:filename/restore` | `ima2 history restore --trash-id` |
533
651
  | `POST /api/history/favorite` | `ima2 history favorite` |
534
652
  | `POST /api/history/import-local` | `ima2 history import` |
535
653
  | `POST /api/metadata/read` | `ima2 metadata` / `ima2 show --metadata` |
@@ -544,10 +662,16 @@ Most server routes under `/api/*` have a CLI wrapper. The exception is **Agent M
544
662
  | `…/api/cardnews/…` (gated on `features.cardNews`) | `ima2 cardnews …` |
545
663
  | `POST /api/comfy/export-image` | `ima2 comfy export` |
546
664
  | `GET /api/inflight` / `DELETE /api/inflight/:id` | `ima2 inflight ls` (alias `ps`) / `ima2 inflight rm` (alias `cancel`) |
665
+ | `GET /api/events` (SSE multiplex) | Web UI only (persistent `EventSource`; no CLI wrapper) |
547
666
  | `GET /api/storage/status` / `POST /api/storage/open-generated-dir` | `ima2 storage status` / `ima2 storage open` |
548
667
  | `GET /api/billing` / `GET /api/providers` / `GET /api/oauth/status` / `GET /api/grok/status` | `ima2 billing` / `ima2 providers` / `ima2 oauth status` / `ima2 grok status` |
668
+ | `GET /api/quota` | `ima2 billing` (includes Grok `usedUsd`/`limitUsd`) |
669
+ | `POST /api/auth/switch` / `GET /api/auth/switch/:sessionId` | Web UI only (Settings > QuotaCard > Switch Account) |
549
670
  | `GET /api/health` | `ima2 ping` |
550
671
  | `GET /api/capabilities` | `ima2 capabilities` |
672
+ | `GET /api/config/grok-planner` | — (Grok planner model query) |
673
+ | `PATCH /api/config/grok-planner` | — (Grok planner model update) |
674
+ | `GET /api/agy/status` | — (Antigravity CLI install status) |
551
675
  | `POST /api/history/backfill-thumbnails` | `ima2 backfill-thumbs` |
552
676
  | `GET /api/keys/status`, `PUT/DELETE /api/keys/:provider`, `PUT/DELETE /api/keys/vertex` | Web UI only (Settings > API Keys) |
553
677
  | `GET/POST/PATCH/DELETE /api/agent/*` (sessions, turns, queue) | — (Agent Mode; web UI only, no CLI) |
@@ -556,7 +680,7 @@ Most server routes under `/api/*` have a CLI wrapper. The exception is **Agent M
556
680
  Notes:
557
681
  - `ima2 history favorite` and `ima2 annotate …` send `X-Ima2-Browser-Id: cli-<sha1prefix>` derived from the config dir, so CLI activity does not collide with browser sessions.
558
682
  - `ima2 session graph save` performs a GET-then-PUT with `If-Match: "<version>"` to guard against `GRAPH_VERSION_CONFLICT`.
559
- - `ima2 history import` and `ima2 canvas-versions save/update` send raw bytes with `Content-Type: image/<png|jpeg|webp>`; the SSE endpoints (`multimode`, `node generate`) use `Accept: text/event-stream`.
683
+ - `ima2 history import` and `ima2 canvas-versions save/update` send raw bytes with `Content-Type: image/<png|jpeg|webp>`; the SSE endpoints (`multimode`, `node generate`, `video`) use `Accept: text/event-stream`. The web UI instead uses `GET /api/events` plus `async: true` on POST routes.
560
684
  - `ima2 cardnews …` checks `runtimeConfig.features.cardNews` before calling the gated endpoints; when disabled the CLI exits 2 with a clear message instead of producing a 404.
561
685
 
562
686
  ## CLI Discovery
package/docs/CLI.md CHANGED
@@ -41,7 +41,7 @@ Agents should start from the packaged skill and capability commands instead of g
41
41
  | `ima2 defaults --json` | Print the running server's effective model/reasoning defaults, falling back to local config when no server is reachable |
42
42
  | `ima2 defaults --local --json` | Print local effective defaults without contacting the server |
43
43
 
44
- `ima2 capabilities --json` separates supported and unsupported model ids. Agents should use only `valid.imageModels.supported` for generation/default choices. `limits.maxParallel` is advisory queue guidance; it is not a server-side concurrency semaphore.
44
+ `ima2 capabilities --json` separates supported and unsupported model ids. Agents should use only `valid.imageModels.supported` for generation/default choices. `limits.maxGeneratedImages` reports the configured per-request image count limit, and `limits.maxParallel` reports the enforced server-side inflight capacity guard.
45
45
 
46
46
  ## Generation
47
47
 
@@ -61,7 +61,8 @@ Provider override semantics:
61
61
  - `api` forces the API-key Responses path and requires a configured API key.
62
62
  - `oauth` forces the local OAuth proxy path.
63
63
  - `grok` uses the bundled progrok xAI proxy (`127.0.0.1:18645`). Classic generation first runs mandatory xAI Web Search through Responses API, then asks `grok-4.3` to call ima2's local `generate_image` tool, then ima2 executes xAI `/v1/images/generations`. If `--ref` images are attached, the final step uses xAI `/v1/images/edits` instead so image-to-image/reference context is preserved. Models: `grok-imagine-image`, `grok-imagine-image-quality`. Size is mapped to xAI `aspect_ratio` and `resolution`; the UI web-search toggle is OpenAI-provider-only because Grok search is always on in this path.
64
- - `agy` spawns the Antigravity CLI to generate via Google Gemini (`nano-banana-2`). Fixed 1024×1024 JPEG output, max 3 refs. No web search, quality, size, or mask controls.
64
+ - `agy` spawns the Antigravity CLI to generate via Google Gemini (`nano-banana-2`). Fixed 1024×1024 JPEG output, max 3 refs. No web search, quality, size, or mask controls. If `agy` is not on the server process PATH, ima2 also checks common user-local installs such as `~/.local/bin/agy`; set `IMA2_AGY_BIN=/absolute/path/to/agy` to force a specific binary.
65
+ - `gemini-api` calls the Google Generative Language API directly. Models: `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). Use `--model nano-banana-2` or `--model nano-banana-pro` to select. Supports `--size` for aspect ratio and resolution (512px–4K) on the direct API path; Vertex AI ignores aspect/size. Requires `GEMINI_API_KEY` or a Vertex AI service account (`VERTEX_SERVICE_ACCOUNT_JSON`). Switching from `agy` or `gemini-api` provider auto-selects the corresponding Gemini model; switching away resets to the GPT default.
65
66
  - `auto` preserves route default behavior and currently resolves to GPT OAuth unless server routing changes.
66
67
 
67
68
  `ima2 serve` starts the bundled Grok proxy automatically. No separate `progrok`
@@ -107,7 +108,7 @@ mockup`.
107
108
  For dense or critical text, keep the text large and explicit. Exact placement,
108
109
  small text, and pixel-perfect typography can still need iteration or post-editing.
109
110
 
110
- Multimode-specific flags include `--max-images <1..8>`, `--ref <file>` (repeatable, max 5), `--mode <auto|direct>`, `--provider <auto|oauth|api|grok|grok-api|agy|gemini-api>`, and `--show-partial`. `ima2 edit --mask` remains intentionally deferred to #31 because current mask plumbing is guided edit rather than guaranteed true masked/inpaint semantics.
111
+ Multimode-specific flags include `--max-images <1..24>` by default (configurable through `IMA2_MAX_GENERATED_IMAGES`), `--ref <file>` (repeatable, max 5), `--mode <auto|direct>`, `--provider <auto|oauth|api|grok|grok-api|agy|gemini-api>`, and `--show-partial`. `ima2 edit --mask` remains intentionally deferred to #31 because current mask plumbing is guided edit rather than guaranteed true masked/inpaint semantics.
111
112
 
112
113
  ## Video
113
114
 
@@ -317,7 +318,7 @@ Card News requires the server to be started with `IMA2_CARD_NEWS=1` (or `feature
317
318
  | `ima2 inflight rm <requestId>` | Force-remove a stuck job |
318
319
  | `ima2 storage status` | Storage inspection (richer than `doctor`) |
319
320
  | `ima2 storage open` | Open the generated dir in the OS file manager (POST) |
320
- | `ima2 billing` | API usage / quota |
321
+ | `ima2 billing` | API usage / quota; Grok result includes `billing.usedUsd` / `billing.limitUsd` drawn from the xAI billing API |
321
322
  | `ima2 providers` | Configured providers |
322
323
  | `ima2 oauth status` | OAuth proxy state |
323
324
  | `ima2 grok status` | Bundled progrok / xAI image-model probe state |
@@ -361,7 +362,7 @@ comfy.{defaultUrl,uploadTimeoutMs,maxUploadBytes}
361
362
  storage.{generatedDir,generatedDirName}
362
363
  server.{port,host,bodyLimit}
363
364
  oauth.{proxyPort,statusTimeoutMs,restartDelayMs}
364
- limits.{maxRefCount,maxParallel}
365
+ limits.{maxRefCount,maxGeneratedImages,maxParallel}
365
366
  history.{defaultPageSize,maxPageCap}
366
367
  ```
367
368
 
package/docs/FAQ.ko.md CHANGED
@@ -323,6 +323,22 @@ export HTTPS_PROXY=http://127.0.0.1:7890
323
323
 
324
324
  GPT OAuth는 OpenAI와 ChatGPT/Codex 관련 호스트 접근이 필요할 수 있습니다. 회사 방화벽, TLS 검사, VPN, 프록시가 흐름을 깨뜨릴 수 있습니다. 로그인 실패와 `failed to fetch`가 반복되면 다른 네트워크에서도 시도해 보세요.
325
325
 
326
+ ## SSE 멀티플렉싱
327
+
328
+ ### 왜 웹 UI가 단일 SSE 연결을 쓰나요?
329
+
330
+ 브라우저는 같은 origin에 대해 동시 HTTP 연결 수를 제한합니다(보통 6개). 여러 이미지를 동시에 생성할 때 각 요청이 SSE 연결을 점유하면, multimode+node+video가 동시에 돌아갈 때 연결이 포화되어 갤러리 썸네일이 멈췄습니다.
331
+
332
+ 이제 웹 UI는 `GET /api/events`로 하나의 SSE 연결만 열고, 모든 생성 진행 이벤트를 멀티플렉싱합니다. 생성 요청은 `async: true`로 보내면 즉시 `202 { requestId }` 응답을 받아 연결을 바로 해제합니다. CLI는 영향 없이 기존 per-request SSE를 그대로 사용합니다.
333
+
334
+ ### SSE 연결이 끊기면 어떻게 되나요?
335
+
336
+ 이벤트 채널 클라이언트가 지수 백오프로 자동 재연결합니다. 재연결 시 `Last-Event-ID`를 보내서 서버의 링 버퍼(최대 2000건)에서 놓친 이벤트를 재전송받습니다. 버퍼에서 이미 사라진 이벤트가 있으면 `replay-gap` 이벤트로 알려줍니다.
337
+
338
+ ### 동시 작업 상한은 얼마인가요?
339
+
340
+ 서버는 동시 생성 작업을 설정값 `limits.maxParallel` 기준으로 제한합니다(기본 `24`, `IMA2_MAX_PARALLEL`로 변경 가능). 초과 요청은 `429`와 `Retry-After: 5`를 받습니다. SSE 엔드포인트 자체는 512개 동시 연결까지 지원합니다.
341
+
326
342
  ## CLI 점검 순서
327
343
 
328
344
  아래 순서대로 확인해 보세요.
package/docs/FAQ.md CHANGED
@@ -103,6 +103,20 @@ ima2 serve
103
103
 
104
104
  If this happens on a company network, a firewall, VPN, proxy, or captive portal may also be blocking the OAuth flow.
105
105
 
106
+ ### How do I use the Gemini providers?
107
+
108
+ Two Gemini providers are available:
109
+
110
+ - **`agy`** — uses the Antigravity CLI (`agy -p`) with no API key needed. Requires the `agy` binary to be installed and logged in. Model is `nano-banana-2`, output is fixed at 1024×1024.
111
+
112
+ - **`gemini-api`** — calls the Google Generative Language API directly. Add a `GEMINI_API_KEY` env var, or configure a key via Settings > API Keys. For Vertex AI, add a service account JSON via Settings or the `VERTEX_SERVICE_ACCOUNT_JSON` env var. When both an API key and Vertex credentials are present, Vertex takes priority. Use the auth-mode dropdown in Settings to switch between `apikey` and `vertex`; the choice is saved and restored automatically.
113
+
114
+ The `gemini-api` provider supports two models: `nano-banana-2` (Gemini 3.1 Flash Image) and `nano-banana-pro` (Gemini 3 Pro Image). The web UI shows aspect-ratio and resolution controls (512px–4K) for `gemini-api`; these are honored only on the direct Gemini API path and are ignored by Vertex AI.
115
+
116
+ ### How do I re-authenticate Grok or Codex without restarting?
117
+
118
+ Use the **Switch Account** button in Settings > QuotaCard for the provider. This starts a device-code OAuth flow: a new browser tab opens the verification URL, you complete the login, and the server automatically picks up the new credentials. The Grok quota bar also shows `$used / $limit` (in USD) drawn from the xAI billing API.
119
+
106
120
  ## Models and quota
107
121
 
108
122
  ### Which model should I use?
@@ -334,6 +348,22 @@ Use the host and port from your proxy client. If `ima2-gen` still fails after th
334
348
 
335
349
  GPT OAuth may require access to OpenAI and ChatGPT/Codex-related hosts. A corporate firewall, TLS inspection, VPN, or proxy can break the flow. Try a different network if login and `failed to fetch` errors keep repeating.
336
350
 
351
+ ## SSE Multiplexing
352
+
353
+ ### Why does the web UI use a single SSE connection?
354
+
355
+ Browsers limit the number of concurrent HTTP connections to the same origin (typically 6). When generating multiple images at once, each generation request used to hold a Server-Sent Events connection open. With multimode, node, and video running simultaneously, the browser would run out of connections and gallery thumbnails would hang.
356
+
357
+ The web UI now opens a single persistent `GET /api/events` SSE connection and all generation progress is multiplexed through it. Generation requests use `async: true` and receive an immediate `202 { requestId }` response, freeing the connection immediately. The CLI is unaffected — it still uses per-request SSE when `async` is not set.
358
+
359
+ ### What happens if the SSE connection drops?
360
+
361
+ The event channel client reconnects automatically with exponential backoff. On reconnect, it sends `Last-Event-ID` so the server can replay missed events from its ring buffer (up to 2000 entries). If events have been evicted from the buffer, the server sends a `replay-gap` event so the client knows some updates may have been lost.
362
+
363
+ ### What is the maximum number of concurrent jobs?
364
+
365
+ The server caps concurrent generation jobs at the configured `limits.maxParallel` value (default `24`, overridable with `IMA2_MAX_PARALLEL`). Additional requests receive `429` with `Retry-After: 5`. The SSE endpoint itself caps at 512 simultaneous connections.
366
+
337
367
  ## CLI troubleshooting checklist
338
368
 
339
369
  Run these in order:
package/docs/README.ko.md CHANGED
@@ -78,7 +78,7 @@ v1.1.22부터 Ctrl+C가 DB, 소켓, 자식 프로세스를 깨끗하게 정리
78
78
  1. **GPT OAuth** — ChatGPT 계정으로 로그인 (무료, 이미지만)
79
79
  2. **Grok OAuth** — xAI/Grok 계정으로 로그인 (이미지 + 영상)
80
80
  3. **Both** — GPT + Grok 둘 다 (전체 기능)
81
- 4. **API Key** — OpenAI API 입력 (유료)
81
+ 4. **Web setup** — UI에서 전체 설정
82
82
 
83
83
  영상 생성은 Grok OAuth(2번 또는 3번)가 필요합니다.
84
84
 
@@ -95,6 +95,10 @@ v1.1.22부터 Ctrl+C가 DB, 소켓, 자식 프로세스를 깨끗하게 정리
95
95
  - **Mobile shell**: 작은 화면에서는 app bar, compose sheet, compact settings toggle로 조작합니다.
96
96
  - **Observable jobs**: 진행 중인 작업과 최근 완료된 작업을 request ID로 추적합니다.
97
97
 
98
+ ### SSE 멀티플렉싱
99
+
100
+ 웹 UI는 단일 `GET /api/events` Server-Sent Events 연결로 모든 생성 진행 상황을 수신합니다. Multimode, node, video 요청은 비동기 POST(`202 { requestId }`)로 제출되고, 이벤트 버스를 통해 진행 이벤트가 멀티플렉싱됩니다. 기존 브라우저 6-연결 제한으로 인한 동시 생성 시 갤러리 hang 문제가 해결됩니다. `async: true`를 보내지 않는 CLI 클라이언트는 기존 per-request SSE 스트림을 그대로 사용할 수 있습니다.
101
+
98
102
  ## 이미지 생성 공급자
99
103
 
100
104
  이미지 생성은 로컬 Codex/ChatGPT OAuth, OpenAI API key, 번들 Grok 공급자를 지원합니다.
@@ -232,8 +236,8 @@ environment variables > ~/.ima2/config.json > built-in defaults
232
236
  | `IMA2_NO_GROK_PROXY` | — | `1`이면 progrok 자동 시작 비활성화 |
233
237
  | `IMA2_GROK_PLANNER_MODEL` | `grok-4.3` | Grok 플래너 모델 (설정 UI 또는 `--planner-model` CLI 플래그로도 변경 가능) |
234
238
  | `IMA2_GROK_IMAGE_MODEL_DEFAULT` | `grok-imagine-image` | 기본 Grok 이미지 모델 |
235
- | `IMA2_LOG_LEVEL` | `warn` | 일반 `serve`는 `warn`, dev 모드는 `debug`. `debug`, `info`, `warn`, `error`, `silent` 지원 |
236
- | `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `30000` | 디버그용 최근 작업 보존 시간 |
239
+ | `IMA2_LOG_LEVEL` | `info` | 일반 `serve`는 `info`, dev 모드는 `debug`. `debug`, `info`, `warn`, `error`, `silent` 지원 |
240
+ | `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `300000` | 디버그용 최근 작업 보존 시간 (5분) |
237
241
  | `OPENAI_API_KEY` | — | `provider: "api"` Responses 이미지 경로와 보조 기능용 API 키 |
238
242
 
239
243
  ### 로그 모드
@@ -4,27 +4,36 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
4
4
 
5
5
  _Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
6
6
 
7
- Total: 177 (runtime: 61, contract: 116)
7
+ Total: 201 (runtime: 71, contract: 130)
8
8
 
9
9
  ## Runtime-importing tests
10
10
  - `tests/agent-mode-auto-planner-contract.test.ts`
11
+ - `tests/agent-mode-llm-planner-contract.test.ts`
11
12
  - `tests/agent-mode-parallel-contract.test.ts`
12
13
  - `tests/agent-mode-queue-contract.test.ts`
13
14
  - `tests/agent-mode-queue-migration-contract.test.ts`
14
15
  - `tests/agent-mode-runtime-contract.test.ts`
15
16
  - `tests/agent-mode-slash-command-contract.test.ts`
17
+ - `tests/agent-mode-ux-feedback-contract.test.ts`
16
18
  - `tests/agent-video-intent.test.ts`
19
+ - `tests/agy-artifact-fallback.test.ts`
20
+ - `tests/agy-cli.test.ts`
17
21
  - `tests/api-cache-policy.test.ts`
18
22
  - `tests/api-provider-parity.test.ts`
19
23
  - `tests/billing-source.test.ts`
20
24
  - `tests/card-news-contract.test.ts`
25
+ - `tests/card-news-template.test.ts`
26
+ - `tests/classic-generate-async.test.ts`
21
27
  - `tests/cli-error-hints.test.ts`
22
28
  - `tests/cli-lib.test.ts`
23
29
  - `tests/comfy-bridge-contract.test.ts`
24
30
  - `tests/error-classify.test.ts`
31
+ - `tests/event-bus.test.ts`
32
+ - `tests/events-channel-contract.test.ts`
25
33
  - `tests/generate-route-validation-error.test.ts`
26
34
  - `tests/generated-static-privacy.test.ts`
27
35
  - `tests/generation-errors.test.ts`
36
+ - `tests/generation-request-log.test.ts`
28
37
  - `tests/grok-command-login-contract.test.ts`
29
38
  - `tests/grok-planner-adapter.test.ts`
30
39
  - `tests/grok-proxy-launcher.test.ts`
@@ -36,6 +45,7 @@ Total: 177 (runtime: 61, contract: 116)
36
45
  - `tests/image-metadata-route.test.ts`
37
46
  - `tests/image-metadata-xmp.test.ts`
38
47
  - `tests/image-model.test.ts`
48
+ - `tests/inflight-guard-contract.test.ts`
39
49
  - `tests/inflight-persistence.test.ts`
40
50
  - `tests/inflight.test.ts`
41
51
  - `tests/local-import-contract.test.ts`
@@ -73,8 +83,12 @@ Total: 177 (runtime: 61, contract: 116)
73
83
  - `tests/agent-mode-frontend-contract.test.js`
74
84
  - `tests/agent-mode-layout-contract.test.js`
75
85
  - `tests/agent-mode-right-sidebar-contract.test.js`
86
+ - `tests/agent-mode-run-progress-contract.test.ts`
76
87
  - `tests/agent-mode-tool-folding-contract.test.js`
77
88
  - `tests/app-weight-splitting-contract.test.js`
89
+ - `tests/async-capacity-retry-behavior.test.ts`
90
+ - `tests/async-capacity-retry-contract.test.js`
91
+ - `tests/async-stream-subscribe-order.test.js`
78
92
  - `tests/background-cleanup-brush-rasterize.test.js`
79
93
  - `tests/background-cleanup-mask-compose.test.js`
80
94
  - `tests/bin.test.js`
@@ -92,6 +106,7 @@ Total: 177 (runtime: 61, contract: 116)
92
106
  - `tests/canvas-hit-test-contract.test.js`
93
107
  - `tests/canvas-mask-contract.test.js`
94
108
  - `tests/canvas-memo-contract.test.js`
109
+ - `tests/canvas-memo-prompt-contract.test.js`
95
110
  - `tests/canvas-mode-contract.test.js`
96
111
  - `tests/canvas-object-key-contract.test.js`
97
112
  - `tests/canvas-persistence-contract.test.js`
@@ -123,13 +138,18 @@ Total: 177 (runtime: 61, contract: 116)
123
138
  - `tests/current-image-actions-readiness-contract.test.js`
124
139
  - `tests/direct-mode-visual-contract.test.js`
125
140
  - `tests/edit-mask-api-contract.test.js`
141
+ - `tests/frontend-connection-state-contract.test.js`
142
+ - `tests/frontend-sse-risk-contract.test.js`
143
+ - `tests/gallery-hang-regression-contract.test.ts`
126
144
  - `tests/gallery-load-older-contract.test.js`
127
145
  - `tests/gallery-navigation-ux-contract.test.js`
146
+ - `tests/gallery-selection-during-generation-contract.test.js`
128
147
  - `tests/gallery-session-scope-contract.test.js`
129
148
  - `tests/gallery-shortcuts-behavior.test.js`
130
149
  - `tests/gallery-shortcuts-visible-domain-contract.test.js`
131
150
  - `tests/gallery-viewer-navigation-contract.test.js`
132
151
  - `tests/generation-controls-ux-contract.test.js`
152
+ - `tests/generation-limit-unlock-contract.test.js`
133
153
  - `tests/health.test.js`
134
154
  - `tests/history-index-contract.test.js`
135
155
  - `tests/history-permanent-delete.test.js`
@@ -145,6 +165,7 @@ Total: 177 (runtime: 61, contract: 116)
145
165
  - `tests/multimode-backend-contract.test.js`
146
166
  - `tests/multimode-concurrent-store-contract.test.js`
147
167
  - `tests/multimode-ui-contract.test.js`
168
+ - `tests/node-async-eventbus-contract.test.js`
148
169
  - `tests/node-batch-contract.test.js`
149
170
  - `tests/node-child-refs-contract.test.js`
150
171
  - `tests/node-child-refs-payload.test.js`
@@ -152,6 +173,7 @@ Total: 177 (runtime: 61, contract: 116)
152
173
  - `tests/node-diagnostics-contract.test.js`
153
174
  - `tests/node-edge-disconnect-contract.test.js`
154
175
  - `tests/node-footer-compact-contract.test.js`
176
+ - `tests/node-gallery-drop-contract.test.js`
155
177
  - `tests/node-generation-lock-contract.test.js`
156
178
  - `tests/node-layout-contract.test.js`
157
179
  - `tests/node-pending-recovery-contract.test.js`
@@ -171,6 +193,7 @@ Total: 177 (runtime: 61, contract: 116)
171
193
  - `tests/prompt-library-ui-contract.test.js`
172
194
  - `tests/prompt-studio-docs-contract.test.js`
173
195
  - `tests/prompt-studio-ui-contract.test.js`
196
+ - `tests/provider-ui-polish-contract.test.js`
174
197
  - `tests/server-fallback-contract.test.js`
175
198
  - `tests/server.test.js`
176
199
  - `tests/session-conflict.test.js`
@@ -184,5 +207,6 @@ Total: 177 (runtime: 61, contract: 116)
184
207
  - `tests/toast-stack-contract.test.js`
185
208
  - `tests/ui-error-code-contract.test.js`
186
209
  - `tests/video-continuity-ui-contract.test.js`
210
+ - `tests/video-gallery-refresh-contract.test.ts`
187
211
  - `tests/vite-dev-port-contract.test.js`
188
212
  - `tests/web-search-toggle-contract.test.js`
@@ -1,3 +1,6 @@
1
+ import { config } from "../config.js";
2
+ const MAX_AGENT_VARIANT_COUNT = Math.max(1, Math.trunc(Number(config.limits.maxGeneratedImages) || 24));
3
+ const MAX_AGENT_PARALLELISM = Math.max(1, Math.trunc(Number(config.limits.maxParallel) || 24));
1
4
  const COMMAND_ALIASES = {
2
5
  ask: "question",
3
6
  q: "question",
@@ -28,7 +31,7 @@ export function parseAgentSlashCommand(input) {
28
31
  return null;
29
32
  const rest = (match[2] ?? "").trim();
30
33
  const countCommands = ["variants", "generate", "parallelism"];
31
- const parsed = countCommands.includes(name) ? parseLeadingCount(rest) : { prompt: rest };
34
+ const parsed = countCommands.includes(name) ? parseLeadingCount(rest, name) : { prompt: rest };
32
35
  return {
33
36
  name,
34
37
  rawName,
@@ -41,9 +44,9 @@ export function formatAgentSlashHelp() {
41
44
  return [
42
45
  "Available Agent commands:",
43
46
  "/question <topic> - ask a planning question without generating images.",
44
- "/variants <1-8> <prompt> - force that many image variants.",
45
- "/generate <1-8> <prompt> - generate a bounded fanout from one request.",
46
- "/parallelism <1-8> <prompt> - cap concurrent tool calls for this turn.",
47
+ `/variants <1-${MAX_AGENT_VARIANT_COUNT}> <prompt> - force that many image variants.`,
48
+ `/generate <1-${MAX_AGENT_VARIANT_COUNT}> <prompt> - generate a bounded fanout from one request.`,
49
+ `/parallelism <1-${MAX_AGENT_PARALLELISM}> <prompt> - cap concurrent tool calls for this turn.`,
47
50
  ].join("\n");
48
51
  }
49
52
  export function formatAgentQuestionReply(prompt) {
@@ -53,12 +56,13 @@ export function formatAgentQuestionReply(prompt) {
53
56
  }
54
57
  return topic;
55
58
  }
56
- function parseLeadingCount(value) {
59
+ function parseLeadingCount(value, name) {
57
60
  const match = /^(\d+)(?:\s+([\s\S]*))?$/.exec(value);
58
61
  if (!match)
59
62
  return { prompt: value };
63
+ const max = name === "parallelism" ? MAX_AGENT_PARALLELISM : MAX_AGENT_VARIANT_COUNT;
60
64
  return {
61
- value: Math.max(1, Math.min(8, Number(match[1]))),
65
+ value: Math.max(1, Math.min(max, Number(match[1]))),
62
66
  prompt: (match[2] ?? "").trim(),
63
67
  };
64
68
  }