@dickpy/dsh-imagegen 1.2.0 → 1.2.2
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.
- package/README.md +117 -139
- package/docs/images/agent-chat-poster-workflow.png +0 -0
- package/docs/images/imagegen-overview.png +0 -0
- package/docs/images/poster-features-16x9.png +0 -0
- package/lib/client.js +468 -254
- package/lib/client.js.map +1 -1
- package/lib/index.js +71 -40
- package/package.json +3 -2
- package/src/agent-image-tools.ts +68 -41
- package/src/client/ImageGenPanel.tsx +20 -10
- package/src/client/image-toolview.module.css +73 -0
- package/src/client/image-toolview.tsx +158 -0
- package/src/client/index.ts +13 -9
- package/src/client/panel.module.css +15 -1
- package/src/index.ts +3 -2
- package/src/protocol.ts +1 -1
package/lib/index.js
CHANGED
|
@@ -6,7 +6,6 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm/message";
|
|
10
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
11
10
|
//#region src/protocol.ts
|
|
12
11
|
/**
|
|
@@ -17,7 +16,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
|
17
16
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
18
17
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
19
18
|
/** Published package version shared by the host updater and the client UI. */
|
|
20
|
-
const PLUGIN_VERSION = "1.2.
|
|
19
|
+
const PLUGIN_VERSION = "1.2.2";
|
|
21
20
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
22
21
|
const SETTINGS_API = {
|
|
23
22
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -2297,6 +2296,8 @@ const taskResultSchema = {
|
|
|
2297
2296
|
}
|
|
2298
2297
|
}
|
|
2299
2298
|
};
|
|
2299
|
+
/** Agent calls stay pending until the provider and history write settle. */
|
|
2300
|
+
const AGENT_GENERATION_TIMEOUT_MS = 3e5;
|
|
2300
2301
|
function acceptedMediaType(value) {
|
|
2301
2302
|
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
2302
2303
|
}
|
|
@@ -2337,7 +2338,6 @@ function renderTaskResult(value) {
|
|
|
2337
2338
|
/** Register the global Agent tools and unregister them with the plugin lifecycle. */
|
|
2338
2339
|
function registerAgentImageTools(ctx, runtime, resolve) {
|
|
2339
2340
|
const attachmentRefs = /* @__PURE__ */ new Map();
|
|
2340
|
-
const taskSubscriptions = /* @__PURE__ */ new Set();
|
|
2341
2341
|
const ensureConfigured = () => {
|
|
2342
2342
|
const config = resolve();
|
|
2343
2343
|
if (!config.enabled) throw new ImageGenError("AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.", "plugin-disabled");
|
|
@@ -2366,7 +2366,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2366
2366
|
return {
|
|
2367
2367
|
task_id: task.id,
|
|
2368
2368
|
status: task.status,
|
|
2369
|
-
message: task.status === "completed" ? "Generation completed. The images are attached below and can be reused as source_image in edit_image." : task.status === "failed" ? "Generation failed." : task.status === "cancelled" ? "Generation was cancelled." : "Generation is still running.
|
|
2369
|
+
message: task.status === "completed" ? "Generation completed. The images are attached below and can be reused as source_image in edit_image." : task.status === "failed" ? "Generation failed." : task.status === "cancelled" ? "Generation was cancelled." : "Generation is still running. Query the task again when you need its current status.",
|
|
2370
2370
|
...task.error === void 0 ? {} : { error: task.error },
|
|
2371
2371
|
images
|
|
2372
2372
|
};
|
|
@@ -2376,39 +2376,66 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2376
2376
|
if (task === void 0) throw new ImageGenError(`Image generation task ${id} was not found.`, "task-not-found");
|
|
2377
2377
|
return task;
|
|
2378
2378
|
};
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2379
|
+
const waitForTask = (id, signal) => new Promise((resolveTask, rejectTask) => {
|
|
2380
|
+
let settled = false;
|
|
2381
|
+
let dispose = () => {};
|
|
2382
|
+
let timer;
|
|
2383
|
+
let abort = () => {};
|
|
2384
|
+
const cleanup = () => {
|
|
2385
|
+
dispose();
|
|
2386
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2387
|
+
signal?.removeEventListener("abort", abort);
|
|
2388
|
+
};
|
|
2389
|
+
const resolve = (task) => {
|
|
2390
|
+
if (settled) return;
|
|
2391
|
+
settled = true;
|
|
2392
|
+
cleanup();
|
|
2393
|
+
resolveTask(task);
|
|
2394
|
+
};
|
|
2395
|
+
const reject = (error) => {
|
|
2396
|
+
if (settled) return;
|
|
2397
|
+
settled = true;
|
|
2398
|
+
cleanup();
|
|
2399
|
+
rejectTask(error);
|
|
2400
|
+
};
|
|
2401
|
+
abort = () => {
|
|
2402
|
+
if (settled) return;
|
|
2403
|
+
const reason = signal?.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Image generation was cancelled.");
|
|
2404
|
+
settled = true;
|
|
2405
|
+
cleanup();
|
|
2406
|
+
runtime.queue.cancel(id);
|
|
2407
|
+
rejectTask(reason);
|
|
2408
|
+
};
|
|
2397
2409
|
const onChange = (updated) => {
|
|
2398
|
-
if (updated.id
|
|
2399
|
-
dispose?.();
|
|
2400
|
-
if (dispose !== void 0) taskSubscriptions.delete(dispose);
|
|
2401
|
-
notifyCompletion(agent, updated).catch(() => {});
|
|
2410
|
+
if (updated.id === id && isFinalTask(updated)) resolve(updated);
|
|
2402
2411
|
};
|
|
2412
|
+
if (signal?.aborted === true) {
|
|
2413
|
+
abort();
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2403
2416
|
dispose = runtime.queue.subscribe(onChange);
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2417
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2418
|
+
timer = setTimeout(() => {
|
|
2419
|
+
if (settled) return;
|
|
2420
|
+
const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1e3} seconds.`, "generation-timeout");
|
|
2421
|
+
settled = true;
|
|
2422
|
+
cleanup();
|
|
2423
|
+
runtime.queue.cancel(id);
|
|
2424
|
+
rejectTask(timeout);
|
|
2425
|
+
}, AGENT_GENERATION_TIMEOUT_MS);
|
|
2426
|
+
let current;
|
|
2427
|
+
try {
|
|
2428
|
+
current = findTask(id);
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
reject(error);
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
if (isFinalTask(current)) resolve(current);
|
|
2434
|
+
});
|
|
2408
2435
|
const disposers = [
|
|
2409
2436
|
ctx.tools.register(defineTool({
|
|
2410
2437
|
name: "generate_image",
|
|
2411
|
-
description: "
|
|
2438
|
+
description: "Generate an image. By default this tool call stays pending until the task reaches a final state, and completed images are returned directly as tool-result attachments without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. Only use models configured for this plugin; omit model to use the first configured image model.",
|
|
2412
2439
|
parameters: {
|
|
2413
2440
|
prompt: {
|
|
2414
2441
|
type: "string",
|
|
@@ -2434,6 +2461,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2434
2461
|
detail: {
|
|
2435
2462
|
type: "string",
|
|
2436
2463
|
description: "Optional provider detail value, for example standard or high."
|
|
2464
|
+
},
|
|
2465
|
+
wait_for_completion: {
|
|
2466
|
+
type: "boolean",
|
|
2467
|
+
description: "Wait for images and return them in this tool result. Defaults to true; set false for background mode."
|
|
2437
2468
|
}
|
|
2438
2469
|
},
|
|
2439
2470
|
output: {
|
|
@@ -2451,13 +2482,12 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2451
2482
|
n: Math.min(4, Math.max(1, args.count ?? 1)),
|
|
2452
2483
|
detail: args.detail ?? ""
|
|
2453
2484
|
});
|
|
2454
|
-
|
|
2455
|
-
return taskResult(task);
|
|
2485
|
+
return taskResult(args.wait_for_completion === false ? task : await waitForTask(task.id, exec.signal));
|
|
2456
2486
|
}
|
|
2457
2487
|
})),
|
|
2458
2488
|
ctx.tools.register(defineTool({
|
|
2459
2489
|
name: "edit_image",
|
|
2460
|
-
description: "
|
|
2490
|
+
description: "Edit an image. By default this tool call stays pending until the task reaches a final state, and completed images are returned directly as tool-result attachments without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. source_image must be an image reference returned by a completed generation or get_image_generation_task; pass that entire object unchanged. Only configured image models are allowed; omit model to use the first configured model.",
|
|
2461
2491
|
parameters: {
|
|
2462
2492
|
prompt: {
|
|
2463
2493
|
type: "string",
|
|
@@ -2488,6 +2518,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2488
2518
|
detail: {
|
|
2489
2519
|
type: "string",
|
|
2490
2520
|
description: "Optional provider detail value."
|
|
2521
|
+
},
|
|
2522
|
+
wait_for_completion: {
|
|
2523
|
+
type: "boolean",
|
|
2524
|
+
description: "Wait for images and return them in this tool result. Defaults to true; set false for background mode."
|
|
2491
2525
|
}
|
|
2492
2526
|
},
|
|
2493
2527
|
output: {
|
|
@@ -2508,13 +2542,12 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2508
2542
|
image: imageDataUrl(reference),
|
|
2509
2543
|
...reference.ref.name === void 0 ? {} : { refName: reference.ref.name }
|
|
2510
2544
|
});
|
|
2511
|
-
|
|
2512
|
-
return taskResult(task);
|
|
2545
|
+
return taskResult(args.wait_for_completion === false ? task : await waitForTask(task.id, exec.signal));
|
|
2513
2546
|
}
|
|
2514
2547
|
})),
|
|
2515
2548
|
ctx.tools.register(defineTool({
|
|
2516
2549
|
name: "get_image_generation_task",
|
|
2517
|
-
description: "
|
|
2550
|
+
description: "Check an image-generation task status. Completed tasks return image references and image attachments for edit_image. Generation tools normally wait for completion, so use this for explicit recovery or status checks.",
|
|
2518
2551
|
parameters: { task_id: {
|
|
2519
2552
|
type: "string",
|
|
2520
2553
|
required: true,
|
|
@@ -2550,8 +2583,6 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2550
2583
|
}))
|
|
2551
2584
|
];
|
|
2552
2585
|
return () => {
|
|
2553
|
-
for (const dispose of taskSubscriptions) dispose();
|
|
2554
|
-
taskSubscriptions.clear();
|
|
2555
2586
|
for (const dispose of disposers) dispose();
|
|
2556
2587
|
};
|
|
2557
2588
|
}
|
|
@@ -2592,7 +2623,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
|
2592
2623
|
/** Order of the announcement section within the tool-guidance band. */
|
|
2593
2624
|
const SECTION_ORDER = 150;
|
|
2594
2625
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
2595
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API,模型由用户在「设置 → 插件 → AI 生图」中检测或手动配置的生图模型列表决定;支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送)。API 地址与密钥在 GUI 设置中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用已配置的生图模型;模型出现在 /models 中不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image`
|
|
2626
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API,模型由用户在「设置 → 插件 → AI 生图」中检测或手动配置的生图模型列表决定;支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送)。API 地址与密钥在 GUI 设置中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用已配置的生图模型;模型出现在 /models 中不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片直接作为工具结果附件返回,不会额外伪造用户消息。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
2596
2627
|
/** Add the live allow-list so an Agent can honor a user's model choice. */
|
|
2597
2628
|
function guidanceFor(imageModels) {
|
|
2598
2629
|
return `${IMAGEGEN_GUIDANCE} 当前允许调用的生图模型:${imageModels.join("、")}。用户指定其中某个模型时,工具参数 model 必须使用该精确名称;未指定时使用列表中的第一个。`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
3
|
"description": "AI 鐢熷浘 (image generation) plugin for the dsh web GUI: text-to-image and image-to-image through a configurable OpenAI-compatible endpoint (gpt-image-2 / grok-imagine-image / dall-e-3, with native xAI Grok Imagine request shaping), with a settings card for api_url / api_key and a sidebar entry opening a split-pane generation studio.",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"inject": [
|
|
18
18
|
"@deepseek-ai/dsh-client-runtime",
|
|
19
19
|
"@deepseek-ai/dsh-client-connection",
|
|
20
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
21
|
+
"@deepseek-ai/dsh-client-ui-tool"
|
|
21
22
|
],
|
|
22
23
|
"platform": "web"
|
|
23
24
|
}
|
package/src/agent-image-tools.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** Agent-facing image-generation tools backed by the shared host queue. */
|
|
2
2
|
|
|
3
3
|
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm/message'
|
|
5
4
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
5
|
import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
|
|
7
6
|
import type {} from '@deepseek-ai/dsh-attachment'
|
|
@@ -61,6 +60,9 @@ const taskResultSchema = {
|
|
|
61
60
|
},
|
|
62
61
|
} as const
|
|
63
62
|
|
|
63
|
+
/** Agent calls stay pending until the provider and history write settle. */
|
|
64
|
+
const AGENT_GENERATION_TIMEOUT_MS = 300_000
|
|
65
|
+
|
|
64
66
|
function acceptedMediaType(value: string): value is ImageMediaType {
|
|
65
67
|
return value === 'image/png' || value === 'image/jpeg' || value === 'image/webp' || value === 'image/gif'
|
|
66
68
|
}
|
|
@@ -106,7 +108,6 @@ function renderTaskResult(value: AgentTaskResult): Array<{ type: 'text'; text: s
|
|
|
106
108
|
/** Register the global Agent tools and unregister them with the plugin lifecycle. */
|
|
107
109
|
export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRuntime, resolve: () => AgentImageToolConfig): () => void {
|
|
108
110
|
const attachmentRefs = new Map<string, Promise<AgentImageRef[]>>()
|
|
109
|
-
const taskSubscriptions = new Set<() => void>()
|
|
110
111
|
const ensureConfigured = (): void => {
|
|
111
112
|
const config = resolve()
|
|
112
113
|
if (!config.enabled) throw new ImageGenError('AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.', 'plugin-disabled')
|
|
@@ -144,7 +145,7 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
144
145
|
? 'Generation failed.'
|
|
145
146
|
: task.status === 'cancelled'
|
|
146
147
|
? 'Generation was cancelled.'
|
|
147
|
-
: 'Generation is still running.
|
|
148
|
+
: 'Generation is still running. Query the task again when you need its current status.',
|
|
148
149
|
...task.error === undefined ? {} : { error: task.error },
|
|
149
150
|
images,
|
|
150
151
|
}
|
|
@@ -154,42 +155,70 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
154
155
|
if (task === undefined) throw new ImageGenError(`Image generation task ${id} was not found.`, 'task-not-found')
|
|
155
156
|
return task
|
|
156
157
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
158
|
+
const waitForTask = (id: string, signal: AbortSignal | undefined): Promise<GenerationTask> => new Promise((resolveTask, rejectTask) => {
|
|
159
|
+
let settled = false
|
|
160
|
+
let dispose = (): void => {}
|
|
161
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
162
|
+
let abort = (): void => {}
|
|
163
|
+
|
|
164
|
+
const cleanup = (): void => {
|
|
165
|
+
dispose()
|
|
166
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
167
|
+
signal?.removeEventListener('abort', abort)
|
|
168
|
+
}
|
|
169
|
+
const resolve = (task: GenerationTask): void => {
|
|
170
|
+
if (settled) return
|
|
171
|
+
settled = true
|
|
172
|
+
cleanup()
|
|
173
|
+
resolveTask(task)
|
|
174
|
+
}
|
|
175
|
+
const reject = (error: unknown): void => {
|
|
176
|
+
if (settled) return
|
|
177
|
+
settled = true
|
|
178
|
+
cleanup()
|
|
179
|
+
rejectTask(error)
|
|
180
|
+
}
|
|
181
|
+
abort = (): void => {
|
|
182
|
+
if (settled) return
|
|
183
|
+
const reason = signal?.reason instanceof Error ? signal.reason : new Error('Image generation was cancelled.')
|
|
184
|
+
// Remove the listener before publishing cancellation so the queue event
|
|
185
|
+
// cannot turn an execution abort into a successful cancelled result.
|
|
186
|
+
settled = true
|
|
187
|
+
cleanup()
|
|
188
|
+
runtime.queue.cancel(id)
|
|
189
|
+
rejectTask(reason)
|
|
190
|
+
}
|
|
178
191
|
const onChange = (updated: GenerationTask): void => {
|
|
179
|
-
if (updated.id
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
192
|
+
if (updated.id === id && isFinalTask(updated)) resolve(updated)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (signal?.aborted === true) {
|
|
196
|
+
abort()
|
|
197
|
+
return
|
|
183
198
|
}
|
|
184
199
|
dispose = runtime.queue.subscribe(onChange)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
200
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
201
|
+
timer = setTimeout(() => {
|
|
202
|
+
if (settled) return
|
|
203
|
+
const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1000} seconds.`, 'generation-timeout')
|
|
204
|
+
settled = true
|
|
205
|
+
cleanup()
|
|
206
|
+
runtime.queue.cancel(id)
|
|
207
|
+
rejectTask(timeout)
|
|
208
|
+
}, AGENT_GENERATION_TIMEOUT_MS)
|
|
209
|
+
let current: GenerationTask
|
|
210
|
+
try {
|
|
211
|
+
current = findTask(id)
|
|
212
|
+
} catch (error) {
|
|
213
|
+
reject(error)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if (isFinalTask(current)) resolve(current)
|
|
217
|
+
})
|
|
189
218
|
const disposers = [
|
|
190
219
|
ctx.tools.register(defineTool({
|
|
191
220
|
name: 'generate_image',
|
|
192
|
-
description: '
|
|
221
|
+
description: 'Generate an image. By default this tool call stays pending until the task reaches a final state, and completed images are returned directly as tool-result attachments without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. Only use models configured for this plugin; omit model to use the first configured image model.',
|
|
193
222
|
parameters: {
|
|
194
223
|
prompt: { type: 'string', required: true, description: 'Detailed image-generation prompt.' },
|
|
195
224
|
model: { type: 'string', description: 'One of the configured image models. Defaults to the first configured model.' },
|
|
@@ -197,6 +226,7 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
197
226
|
quality: { type: 'string', description: 'auto, 1k, 2k, or 4k.' },
|
|
198
227
|
count: { type: 'integer', description: 'Number of images, 1 to 4. Defaults to 1.' },
|
|
199
228
|
detail: { type: 'string', description: 'Optional provider detail value, for example standard or high.' },
|
|
229
|
+
wait_for_completion: { type: 'boolean', description: 'Wait for images and return them in this tool result. Defaults to true; set false for background mode.' },
|
|
200
230
|
},
|
|
201
231
|
output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
|
|
202
232
|
async execute(args, exec) {
|
|
@@ -210,13 +240,12 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
210
240
|
n: Math.min(4, Math.max(1, args.count ?? 1)),
|
|
211
241
|
detail: args.detail ?? '',
|
|
212
242
|
})
|
|
213
|
-
|
|
214
|
-
return taskResult(task)
|
|
243
|
+
return taskResult(args.wait_for_completion === false ? task : await waitForTask(task.id, exec.signal))
|
|
215
244
|
},
|
|
216
245
|
})),
|
|
217
246
|
ctx.tools.register(defineTool({
|
|
218
247
|
name: 'edit_image',
|
|
219
|
-
description: '
|
|
248
|
+
description: 'Edit an image. By default this tool call stays pending until the task reaches a final state, and completed images are returned directly as tool-result attachments without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. source_image must be an image reference returned by a completed generation or get_image_generation_task; pass that entire object unchanged. Only configured image models are allowed; omit model to use the first configured model.',
|
|
220
249
|
parameters: {
|
|
221
250
|
prompt: { type: 'string', required: true, description: 'How to transform the source image.' },
|
|
222
251
|
source_image: { ...imageRefSchema, required: true, description: 'Image reference returned by get_image_generation_task.' },
|
|
@@ -225,6 +254,7 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
225
254
|
quality: { type: 'string', description: 'auto, 1k, 2k, or 4k.' },
|
|
226
255
|
count: { type: 'integer', description: 'Number of images, 1 to 4. Defaults to 1.' },
|
|
227
256
|
detail: { type: 'string', description: 'Optional provider detail value.' },
|
|
257
|
+
wait_for_completion: { type: 'boolean', description: 'Wait for images and return them in this tool result. Defaults to true; set false for background mode.' },
|
|
228
258
|
},
|
|
229
259
|
output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
|
|
230
260
|
async execute(args, exec) {
|
|
@@ -241,13 +271,12 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
241
271
|
image: imageDataUrl(reference),
|
|
242
272
|
...reference.ref.name === undefined ? {} : { refName: reference.ref.name },
|
|
243
273
|
})
|
|
244
|
-
|
|
245
|
-
return taskResult(task)
|
|
274
|
+
return taskResult(args.wait_for_completion === false ? task : await waitForTask(task.id, exec.signal))
|
|
246
275
|
},
|
|
247
276
|
})),
|
|
248
277
|
ctx.tools.register(defineTool({
|
|
249
278
|
name: 'get_image_generation_task',
|
|
250
|
-
description: '
|
|
279
|
+
description: 'Check an image-generation task status. Completed tasks return image references and image attachments for edit_image. Generation tools normally wait for completion, so use this for explicit recovery or status checks.',
|
|
251
280
|
parameters: { task_id: { type: 'string', required: true, description: 'Task id returned by generate_image or edit_image.' } },
|
|
252
281
|
output: { schema: taskResultSchema, render: (_args, value) => renderTaskResult(value) },
|
|
253
282
|
async execute(args) {
|
|
@@ -269,8 +298,6 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
269
298
|
})),
|
|
270
299
|
]
|
|
271
300
|
return () => {
|
|
272
|
-
for (const dispose of taskSubscriptions) dispose()
|
|
273
|
-
taskSubscriptions.clear()
|
|
274
301
|
for (const dispose of disposers) dispose()
|
|
275
302
|
}
|
|
276
303
|
}
|
|
@@ -213,6 +213,7 @@ export function ImageGenPanel(props: {
|
|
|
213
213
|
const [updateResult, setUpdateResult] = useState<'success' | 'failed' | null>(null)
|
|
214
214
|
const [libraryOpen, setLibraryOpen] = useState(false)
|
|
215
215
|
const [tasks, setTasks] = useState<GenerationTask[]>([])
|
|
216
|
+
const [taskTrayOpen, setTaskTrayOpen] = useState(false)
|
|
216
217
|
const [comparison, setComparison] = useState<ComparisonSession | null>(null)
|
|
217
218
|
const [comparisonFullscreen, setComparisonFullscreen] = useState(false)
|
|
218
219
|
const fileInput = useRef<HTMLInputElement>(null)
|
|
@@ -1152,16 +1153,25 @@ export function ImageGenPanel(props: {
|
|
|
1152
1153
|
</div>
|
|
1153
1154
|
) : null}
|
|
1154
1155
|
{tab !== 'gallery' && tasks.length > 0 ? (
|
|
1155
|
-
<section className={css.taskTray} aria-label={tt('tasks.title')}>
|
|
1156
|
-
<header className={css.taskTrayHeader}>
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
<span className={css.
|
|
1160
|
-
<span className={css.
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1156
|
+
<section className={css.taskTray} data-open={taskTrayOpen ? 'true' : 'false'} aria-label={tt('tasks.title')}>
|
|
1157
|
+
<header className={css.taskTrayHeader}>
|
|
1158
|
+
<button type="button" className={css.taskTrayToggle} aria-expanded={taskTrayOpen} onClick={() => { setTaskTrayOpen(open => !open) }}>
|
|
1159
|
+
<span>{tt('tasks.title')}</span>
|
|
1160
|
+
<span className={css.taskTrayCount}>{tasks.filter(task => task.status === 'queued' || task.status === 'running').length}</span>
|
|
1161
|
+
<span className={css.taskTrayChevron} aria-hidden="true">{taskTrayOpen ? '⌃' : '⌄'}</span>
|
|
1162
|
+
</button>
|
|
1163
|
+
{taskTrayOpen ? <button type="button" className={css.taskTrayClose} aria-label={tt('preview.close')} onClick={() => { setTaskTrayOpen(false) }}>×</button> : null}
|
|
1164
|
+
</header>
|
|
1165
|
+
<div className={css.taskRows}>
|
|
1166
|
+
{tasks.slice(0, 5).map(task => (
|
|
1167
|
+
<div key={task.id} className={css.taskRow} data-status={task.status}>
|
|
1168
|
+
<span className={css.taskStatus}>{tt(`tasks.${task.status}` as never)}</span>
|
|
1169
|
+
<span className={css.taskPrompt}>{task.request.prompt}</span>
|
|
1170
|
+
{(task.status === 'queued' || task.status === 'running') ? <button type="button" onClick={() => { void api.taskCancel(task.id) }}>{tt('tasks.cancel')}</button> : null}
|
|
1171
|
+
{task.status === 'failed' || task.status === 'cancelled' ? <button type="button" onClick={() => { void api.taskRetry(task.id) }}>{tt('tasks.retry')}</button> : null}
|
|
1172
|
+
</div>
|
|
1173
|
+
))}
|
|
1174
|
+
</div>
|
|
1165
1175
|
</section>
|
|
1166
1176
|
) : null}
|
|
1167
1177
|
{tab !== 'gallery' && comparison !== null ? (
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 7px;
|
|
5
|
+
margin: 4px 0;
|
|
6
|
+
padding: 8px 10px 10px;
|
|
7
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
8
|
+
border-radius: 8px;
|
|
9
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.header {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 7px;
|
|
16
|
+
min-height: 20px;
|
|
17
|
+
color: var(--dsw-alias-label-secondary);
|
|
18
|
+
font-size: 12px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.icon {
|
|
22
|
+
color: var(--dsw-alias-brand-primary);
|
|
23
|
+
font-size: 15px;
|
|
24
|
+
line-height: 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.status {
|
|
28
|
+
margin-left: auto;
|
|
29
|
+
color: var(--dsw-alias-label-tertiary);
|
|
30
|
+
font-size: 11px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.message,
|
|
34
|
+
.loading,
|
|
35
|
+
.error {
|
|
36
|
+
margin: 0;
|
|
37
|
+
color: var(--dsw-alias-label-tertiary);
|
|
38
|
+
font-size: 12px;
|
|
39
|
+
line-height: 1.5;
|
|
40
|
+
overflow-wrap: anywhere;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.images {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
gap: 8px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.imageLink {
|
|
50
|
+
display: block;
|
|
51
|
+
max-width: min(280px, 100%);
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
54
|
+
border-radius: 6px;
|
|
55
|
+
background: var(--dsw-alias-bg-base);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.imageLink:hover {
|
|
59
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.image {
|
|
63
|
+
display: block;
|
|
64
|
+
width: auto;
|
|
65
|
+
max-width: 280px;
|
|
66
|
+
height: auto;
|
|
67
|
+
max-height: 220px;
|
|
68
|
+
object-fit: contain;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.error {
|
|
72
|
+
color: var(--dsw-alias-state-error-primary);
|
|
73
|
+
}
|