Project Files
src / tools / inpaint.ts
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { InpaintToolParamsShape, formatToolMetaBlock, setActiveChatContext, syncAttachmentsToState } from "../core-bundle.mjs";
import path from "path";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { quality: _hiddenQuality, ...InpaintToolParamsShapeAgent } = InpaintToolParamsShape;
export function createInpaintTool(ctl: ToolsProviderController): Tool {
return tool({
name: "inpaint",
description: `Repaint a masked region of the canvas by re-generating the area inside the mask.
The area inside the mask is changed — the model fills it with new content based on the prompt.
The area outside the mask is protected and remains unchanged.
The canvas itself is never modified; the result is saved as a new image.
When canvas is a prior 'mask' result (e.g. 'i8'), the mask region is picked up automatically — no extra parameters needed.
Alternatively, use detectLabel to target a detected object without a prior mask step:
- detectLabel: label to match (e.g. 'cat', 'a person') — requires a prior detect_object run.
- detectIndex: zero-based index when multiple matches exist (default: 0).
- frameAdjust: expand (+) or shrink (-) the detection box. Number = % of bbox diagonal; string with 'px' suffix = pixels. E.g. 5, '20px', '-10%'.
canvas may be the original source (e.g. 'a1') or the annotated detect_object result (e.g. 'i3').
Parameters:
- canvas: Source image. Required. Notation: 'a1', 'v2', 'p1', 'i3'.
- prompt: What should appear in the repainted region. Default: empty (preserve content).
- model: Model preset for re-rendering. Default: 'auto'.
- width / height: Output dimensions. Default: derived from canvas.
Returns: Inline preview, JSON with file URLs, model info, and clickable links.
${formatToolMetaBlock()}`,
parameters: InpaintToolParamsShapeAgent as Record<string, z.ZodTypeAny>,
implementation: async (args: any, ctx: any) => {
const onProgress = (step: number, totalSteps: number | undefined, message?: string) => {
try {
if (step === -1 && message) { ctx.status(message); return; }
if (totalSteps && totalSteps > 0) {
ctx.status(`Step ${step}/${totalSteps} (${Math.round((step / (totalSteps + 1)) * 100)}%)`);
} else {
ctx.status(`Step ${step}...`);
}
} catch {}
};
try {
const workingDir = ctl.getWorkingDirectory();
if (typeof workingDir === "string" && workingDir.trim().length > 0) {
const chatId = path.basename(workingDir);
if (/^\d+$/.test(chatId)) {
setActiveChatContext({ chatId, workingDir, requestId: `tool-${Date.now()}` });
}
}
} catch {}
try {
const workingDir = ctl.getWorkingDirectory();
if (typeof workingDir === "string" && workingDir.trim().length > 0) {
await syncAttachmentsToState(workingDir, false, Number.MAX_SAFE_INTEGER);
}
} catch {}
const mod = await import("../core/tools.js");
const resp = await mod.handleInpaint(args, onProgress);
if (resp && Array.isArray(resp.content)) return resp.content;
return typeof resp === "string" ? resp : JSON.stringify(resp);
},
});
}