@rol1n/image-generate 0.0.0-stage → 0.1.1
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 +24 -2
- package/files.ts +49 -0
- package/index.ts +220 -0
- package/model-selector.ts +150 -0
- package/package.json +40 -4
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# image-generate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate images through OpenRouter or DashScope and save them to `.pi/generated-images/` in the current project. Registers the `image_generate` tool and `/image-generate` model selector.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pi install npm:@rol1n/image-generate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Also included in `@rol1n/pi-kit`. Installing this package automatically installs `@rol1n/custom-providers`.
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
Run `/image-generate model` to open the interactive model picker, or `/image-generate model qwen-image-3.0-pro` to switch directly.
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"prompt": "A clean technical diagram of a terminal workflow",
|
|
20
|
+
"aspectRatio": "16:9",
|
|
21
|
+
"model": "google/gemini-3.1-flash-image"
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Authenticate via `/login openrouter` or `/login dashscope` (or `OPENROUTER_API_KEY` / `DASHSCOPE_API_KEY`).
|
package/files.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, open, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_OUTPUT_DIR = ".pi/generated-images";
|
|
6
|
+
|
|
7
|
+
function detectExtension(mimeType: string): string {
|
|
8
|
+
switch (mimeType) {
|
|
9
|
+
case "image/jpeg":
|
|
10
|
+
case "image/jpg":
|
|
11
|
+
return ".jpg";
|
|
12
|
+
case "image/webp":
|
|
13
|
+
return ".webp";
|
|
14
|
+
case "image/gif":
|
|
15
|
+
return ".gif";
|
|
16
|
+
case "image/bmp":
|
|
17
|
+
return ".bmp";
|
|
18
|
+
default:
|
|
19
|
+
return ".png";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function saveImageAtomically(cwd: string, buffer: Buffer, mimeType: string): Promise<string> {
|
|
24
|
+
const outputDir = resolve(cwd, DEFAULT_OUTPUT_DIR);
|
|
25
|
+
if (!existsSync(outputDir)) {
|
|
26
|
+
await mkdir(outputDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const date = new Date().toISOString().replace(/T/, "-").replace(/:/g, "").slice(0, 15);
|
|
30
|
+
const ext = detectExtension(mimeType);
|
|
31
|
+
|
|
32
|
+
for (let i = 1; i <= 999; i++) {
|
|
33
|
+
const filename = `${date}-${String(i).padStart(3, "0")}${ext}`;
|
|
34
|
+
const targetPath = join(outputDir, filename);
|
|
35
|
+
try {
|
|
36
|
+
const handle = await open(targetPath, "wx");
|
|
37
|
+
await handle.writeFile(buffer);
|
|
38
|
+
await handle.close();
|
|
39
|
+
return targetPath;
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
if (err?.code === "EEXIST") continue;
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const fallback = join(outputDir, `${date}-${Date.now()}${ext}`);
|
|
47
|
+
await writeFile(fallback, buffer);
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* image-generate — Image generation tool and model selector.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* 1. Tool `image_generate`: Tool call for generating images with local disk persistence.
|
|
6
|
+
* Delegates generation to adapters in custom-providers/image (DashScope and OpenRouter).
|
|
7
|
+
* 2. Command `/image-generate model`: Interactive model picker matching Pi's native model selector UI.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { Text, type AutocompleteItem } from "@earendil-works/pi-tui";
|
|
12
|
+
import { Type } from "typebox";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ensureCustomProviders,
|
|
16
|
+
fetchAllAvailableImageModels,
|
|
17
|
+
generateImage,
|
|
18
|
+
resolveImageModel,
|
|
19
|
+
type ImageModelDefinition,
|
|
20
|
+
} from "@rol1n/custom-providers";
|
|
21
|
+
import { saveImageAtomically } from "./files.ts";
|
|
22
|
+
/** Image catalog lookup and provider selection. */
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_MODEL = "google/gemini-3.1-flash-image";
|
|
25
|
+
|
|
26
|
+
/** Image models known to Pi: OpenRouter's built-in catalog plus our DashScope registration. */
|
|
27
|
+
export function registryImageModels(ctx: { modelRegistry: { getModelsOfType(type: string): readonly any[] } }) {
|
|
28
|
+
return ctx.modelRegistry.getModelsOfType("image") as readonly { id: string; name?: string; provider: string }[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Resolve which provider owns a model id, preferring the Pi registry over heuristics. */
|
|
32
|
+
export function resolveProviderId(
|
|
33
|
+
ctx: { modelRegistry: { getModelsOfType(type: string): readonly any[] } },
|
|
34
|
+
modelId: string,
|
|
35
|
+
): string {
|
|
36
|
+
const lower = modelId.toLowerCase();
|
|
37
|
+
const known = registryImageModels(ctx).find((m) => m.id.toLowerCase() === lower);
|
|
38
|
+
if (known) return known.provider;
|
|
39
|
+
return lower.startsWith("qwen-image") ? "dashscope" : "openrouter";
|
|
40
|
+
}
|
|
41
|
+
import { SearchableImageModelSelectorComponent } from "./model-selector.ts";
|
|
42
|
+
|
|
43
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
44
|
+
|
|
45
|
+
let currentModel = DEFAULT_MODEL;
|
|
46
|
+
|
|
47
|
+
// ============================ Extension Entrypoint ============================
|
|
48
|
+
|
|
49
|
+
export default function imageGenerate(pi: ExtensionAPI): void {
|
|
50
|
+
ensureCustomProviders(pi);
|
|
51
|
+
// 1. Slash command: /image-generate model [name]
|
|
52
|
+
pi.registerCommand("image-generate", {
|
|
53
|
+
description: "Switch default image generation model (/image-generate model [name])",
|
|
54
|
+
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
55
|
+
const trimmed = prefix.trimStart();
|
|
56
|
+
if (!trimmed || "model".startsWith(trimmed)) {
|
|
57
|
+
return [{ value: "model", label: "model", description: "Select image generation model in TUI" }];
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
},
|
|
61
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
62
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
63
|
+
const sub = (parts[0] || "").toLowerCase();
|
|
64
|
+
|
|
65
|
+
const models = await fetchAllAvailableImageModels(registryImageModels(ctx), ctx.signal);
|
|
66
|
+
|
|
67
|
+
// Case: direct model slug provided
|
|
68
|
+
if (sub === "model" && parts[1]) {
|
|
69
|
+
const resolved = resolveImageModel(parts.slice(1).join(" "), models);
|
|
70
|
+
currentModel = resolved;
|
|
71
|
+
ctx.ui.notify(`Default image model set to: ${resolved}`, "info");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Case: bare command or /image-generate model -> open interactive selector
|
|
76
|
+
if (sub === "model" || parts.length === 0) {
|
|
77
|
+
// Detect login status for each provider
|
|
78
|
+
const loginStatusMap = new Map<string, boolean>();
|
|
79
|
+
for (const m of models) {
|
|
80
|
+
if (!loginStatusMap.has(m.provider)) {
|
|
81
|
+
const key = await ctx.modelRegistry.getApiKeyForProvider(m.provider);
|
|
82
|
+
loginStatusMap.set(
|
|
83
|
+
m.provider,
|
|
84
|
+
Boolean(key) || Boolean(process.env[`${m.provider.toUpperCase()}_API_KEY`]),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const chosenModel = await ctx.ui.custom<ImageModelDefinition | undefined>((tui, theme, _kb, done) => {
|
|
90
|
+
return new SearchableImageModelSelectorComponent(
|
|
91
|
+
tui,
|
|
92
|
+
theme,
|
|
93
|
+
"Select Image Generation Model",
|
|
94
|
+
models,
|
|
95
|
+
currentModel,
|
|
96
|
+
(model) => done(model),
|
|
97
|
+
() => done(undefined),
|
|
98
|
+
(provider) => loginStatusMap.get(provider) ?? false,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!chosenModel) return;
|
|
103
|
+
|
|
104
|
+
currentModel = chosenModel.id;
|
|
105
|
+
const isLogged = loginStatusMap.get(chosenModel.provider);
|
|
106
|
+
const extra = isLogged ? "" : ` (No key detected: please run /login ${chosenModel.provider})`;
|
|
107
|
+
ctx.ui.notify(`Default image model set to: ${chosenModel.name} (${chosenModel.id})${extra}`, "info");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Unknown arguments: report usage instead of silently doing nothing
|
|
112
|
+
ctx.ui.notify(
|
|
113
|
+
`Unknown argument: ${parts.join(" ")}. Usage: /image-generate [model] [name]`,
|
|
114
|
+
"warning",
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 2. Tool: image_generate (English Schema)
|
|
120
|
+
pi.registerTool({
|
|
121
|
+
name: "image_generate",
|
|
122
|
+
description:
|
|
123
|
+
"Generate an image from a text prompt using OpenRouter or DashScope. Saves the generated image to local disk and returns the file path.",
|
|
124
|
+
parameters: Type.Object({
|
|
125
|
+
prompt: Type.String({
|
|
126
|
+
description: "The text prompt describing the image to generate.",
|
|
127
|
+
}),
|
|
128
|
+
model: Type.Optional(
|
|
129
|
+
Type.String({
|
|
130
|
+
description:
|
|
131
|
+
"Optional model slug (e.g. 'google/gemini-3.1-flash-image', 'qwen-image-3.0-pro', 'black-forest-labs/flux.2-pro'). Defaults to current default model.",
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
aspectRatio: Type.Optional(
|
|
135
|
+
Type.String({
|
|
136
|
+
description: "Optional aspect ratio for the image (e.g., '1:1', '16:9', '9:16', '4:3', '3:4').",
|
|
137
|
+
}),
|
|
138
|
+
),
|
|
139
|
+
image: Type.Optional(
|
|
140
|
+
Type.String({
|
|
141
|
+
description: "Optional local file path or remote image URL for image-to-image reference.",
|
|
142
|
+
}),
|
|
143
|
+
),
|
|
144
|
+
}),
|
|
145
|
+
execute: async (_toolCallId, params, signal, ctx) => {
|
|
146
|
+
const targetModel = params.model
|
|
147
|
+
? resolveImageModel(params.model, registryImageModels(ctx))
|
|
148
|
+
: currentModel;
|
|
149
|
+
const timeoutSig = AbortSignal.any([AbortSignal.timeout(DEFAULT_TIMEOUT_MS), ...(signal ? [signal] : [])]);
|
|
150
|
+
|
|
151
|
+
const providerId = resolveProviderId(ctx, targetModel);
|
|
152
|
+
const apiKey =
|
|
153
|
+
(await ctx.modelRegistry.getApiKeyForProvider(providerId)) ||
|
|
154
|
+
process.env[`${providerId.toUpperCase()}_API_KEY`];
|
|
155
|
+
|
|
156
|
+
if (!apiKey) {
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: `Error: No ${providerId} API key found. Please run '/login ${providerId}' or set ${providerId.toUpperCase()}_API_KEY environment variable.`,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
isError: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const res = await generateImage(
|
|
170
|
+
{
|
|
171
|
+
prompt: params.prompt,
|
|
172
|
+
model: targetModel,
|
|
173
|
+
aspectRatio: params.aspectRatio,
|
|
174
|
+
image: params.image,
|
|
175
|
+
cwd: ctx.cwd,
|
|
176
|
+
},
|
|
177
|
+
{ apiKey, provider: providerId, signal: timeoutSig, cwd: ctx.cwd },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const savedPath = await saveImageAtomically(ctx.cwd, res.buffer, res.mimeType);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: `Image successfully generated using '${res.model}'.\nSaved to: ${savedPath}`,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
details: {
|
|
190
|
+
model: res.model,
|
|
191
|
+
provider: res.provider,
|
|
192
|
+
savedPath,
|
|
193
|
+
prompt: params.prompt,
|
|
194
|
+
revisedPrompt: res.revisedPrompt,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
} catch (err: any) {
|
|
198
|
+
return {
|
|
199
|
+
content: [
|
|
200
|
+
{
|
|
201
|
+
type: "text",
|
|
202
|
+
text: `Image generation failed: ${err?.message ?? String(err)}`,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
isError: true,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
renderCall(args, theme) {
|
|
210
|
+
const { prompt, model } = args as { prompt?: string; model?: string };
|
|
211
|
+
const displayModel = model || currentModel;
|
|
212
|
+
const preview = prompt ? (prompt.length > 50 ? prompt.slice(0, 47) + "..." : prompt) : "";
|
|
213
|
+
return new Text(
|
|
214
|
+
theme.fg("toolTitle", theme.bold("generate_image ")) +
|
|
215
|
+
theme.fg("dim", `[${displayModel}] `) +
|
|
216
|
+
theme.fg("accent", `"${preview}"`),
|
|
217
|
+
);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Container, Input, Spacer, Text, fuzzyFilter, getKeybindings } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { ImageModelDefinition } from "@rol1n/custom-providers";
|
|
3
|
+
|
|
4
|
+
export class SearchableImageModelSelectorComponent extends Container {
|
|
5
|
+
private searchInput: Input;
|
|
6
|
+
private listContainer: Container;
|
|
7
|
+
private selectedIndex = 0;
|
|
8
|
+
private filteredModels: ImageModelDefinition[] = [];
|
|
9
|
+
private _focused = true;
|
|
10
|
+
|
|
11
|
+
get focused() {
|
|
12
|
+
return this._focused;
|
|
13
|
+
}
|
|
14
|
+
set focused(value: boolean) {
|
|
15
|
+
this._focused = value;
|
|
16
|
+
this.searchInput.focused = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private tui: any,
|
|
21
|
+
private theme: any,
|
|
22
|
+
private title: string,
|
|
23
|
+
private models: ImageModelDefinition[],
|
|
24
|
+
private currentModelId: string,
|
|
25
|
+
private onSelect: (model: ImageModelDefinition) => void,
|
|
26
|
+
private onCancel: () => void,
|
|
27
|
+
private getLoginStatus?: (provider: string) => boolean,
|
|
28
|
+
) {
|
|
29
|
+
super();
|
|
30
|
+
this.filteredModels = [...models];
|
|
31
|
+
const curIdx = models.findIndex((m) => m.id === currentModelId);
|
|
32
|
+
this.selectedIndex = curIdx >= 0 ? curIdx : 0;
|
|
33
|
+
|
|
34
|
+
this.searchInput = new Input();
|
|
35
|
+
this.searchInput.focused = true;
|
|
36
|
+
this.listContainer = new Container();
|
|
37
|
+
|
|
38
|
+
this.updateList();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
filterModels(query: string) {
|
|
42
|
+
const q = query.trim().toLowerCase();
|
|
43
|
+
if (q) {
|
|
44
|
+
this.filteredModels = fuzzyFilter(
|
|
45
|
+
this.models,
|
|
46
|
+
q,
|
|
47
|
+
(m) => `${m.id} ${m.name} ${m.provider} ${m.description}`,
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
this.filteredModels = [...this.models];
|
|
51
|
+
}
|
|
52
|
+
this.selectedIndex = q ? 0 : Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
53
|
+
this.updateList();
|
|
54
|
+
this.tui.requestRender?.();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
updateList() {
|
|
58
|
+
this.listContainer.clear();
|
|
59
|
+
const maxVisible = 6;
|
|
60
|
+
const total = this.filteredModels.length;
|
|
61
|
+
|
|
62
|
+
if (total === 0) {
|
|
63
|
+
this.listContainer.addChild(new Text(this.theme.fg("muted", " No matching models"), 0, 0));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
|
|
68
|
+
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
69
|
+
|
|
70
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
71
|
+
const item = this.filteredModels[i];
|
|
72
|
+
const isSelected = i === this.selectedIndex;
|
|
73
|
+
const isCurrent = item.id === this.currentModelId;
|
|
74
|
+
|
|
75
|
+
const cursor = isSelected ? this.theme.fg("accent", "→ ") : " ";
|
|
76
|
+
const check = isCurrent ? this.theme.fg("accent", "✓ ") : " ";
|
|
77
|
+
const modelText = isSelected
|
|
78
|
+
? this.theme.fg("accent", this.theme.bold ? this.theme.bold(item.id) : item.id)
|
|
79
|
+
: this.theme.fg("text", item.id);
|
|
80
|
+
const providerBadge = this.theme.fg("muted", `[${item.provider}]`);
|
|
81
|
+
|
|
82
|
+
let authBadge = "";
|
|
83
|
+
if (this.getLoginStatus) {
|
|
84
|
+
const loggedIn = this.getLoginStatus(item.provider);
|
|
85
|
+
authBadge = loggedIn
|
|
86
|
+
? this.theme.fg("dim", " · logged in")
|
|
87
|
+
: this.theme.fg("warning", ` · not logged in: /login ${item.provider}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const line = `${cursor}${check}${modelText} ${providerBadge}${authBadge}`;
|
|
91
|
+
this.listContainer.addChild(new Text(line, 0, 0));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (startIndex > 0 || endIndex < total) {
|
|
95
|
+
const scrollInfo = this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`);
|
|
96
|
+
this.listContainer.addChild(new Text(scrollInfo, 0, 0));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const selected = this.filteredModels[this.selectedIndex];
|
|
100
|
+
if (selected) {
|
|
101
|
+
this.listContainer.addChild(new Spacer(1));
|
|
102
|
+
this.listContainer.addChild(
|
|
103
|
+
new Text(this.theme.fg("muted", ` Model: ${selected.name} — ${selected.description}`), 0, 0),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render(width: number): string[] {
|
|
109
|
+
this.clear();
|
|
110
|
+
const border = this.theme.fg("accent", "─".repeat(Math.max(10, width)));
|
|
111
|
+
|
|
112
|
+
this.addChild(new Text(border, 0, 0));
|
|
113
|
+
this.addChild(new Spacer(1));
|
|
114
|
+
this.addChild(new Text(this.theme.fg("accent", this.theme.bold ? this.theme.bold(` ${this.title}`) : ` ${this.title}`), 0, 0));
|
|
115
|
+
this.addChild(new Spacer(1));
|
|
116
|
+
this.addChild(this.searchInput);
|
|
117
|
+
this.addChild(new Spacer(1));
|
|
118
|
+
this.addChild(this.listContainer);
|
|
119
|
+
this.addChild(new Spacer(1));
|
|
120
|
+
this.addChild(new Text(this.theme.fg("dim", " Enter to select · Escape to cancel"), 0, 0));
|
|
121
|
+
this.addChild(new Text(border, 0, 0));
|
|
122
|
+
|
|
123
|
+
return super.render(width);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
handleInput(keyData: string) {
|
|
127
|
+
const kb = getKeybindings();
|
|
128
|
+
if (kb.matches(keyData, "tui.select.up") || keyData === "k") {
|
|
129
|
+
if (this.filteredModels.length === 0) return;
|
|
130
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
|
|
131
|
+
this.updateList();
|
|
132
|
+
this.tui.requestRender?.();
|
|
133
|
+
} else if (kb.matches(keyData, "tui.select.down") || keyData === "j") {
|
|
134
|
+
if (this.filteredModels.length === 0) return;
|
|
135
|
+
this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
|
|
136
|
+
this.updateList();
|
|
137
|
+
this.tui.requestRender?.();
|
|
138
|
+
} else if (kb.matches(keyData, "tui.select.confirm") || keyData === "\n" || keyData === "\r") {
|
|
139
|
+
const selected = this.filteredModels[this.selectedIndex];
|
|
140
|
+
if (selected) {
|
|
141
|
+
this.onSelect(selected);
|
|
142
|
+
}
|
|
143
|
+
} else if (kb.matches(keyData, "tui.select.cancel") || keyData === "\x1b" || keyData === "\x03") {
|
|
144
|
+
this.onCancel();
|
|
145
|
+
} else {
|
|
146
|
+
this.searchInput.handleInput(keyData);
|
|
147
|
+
this.filterModels(this.searchInput.getValue());
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rol1n/image-generate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "image-generate extension from pi-kit",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package"
|
|
8
|
+
],
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/RolinShmily/pi-kit.git",
|
|
12
|
+
"directory": "image-generate"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/RolinShmily/pi-kit#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/RolinShmily/pi-kit/issues"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.19.0"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@rol1n/custom-providers": "^0.1.1"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@earendil-works/pi-ai": "*",
|
|
29
|
+
"@earendil-works/pi-agent-core": "*",
|
|
30
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
31
|
+
"@earendil-works/pi-tui": "*",
|
|
32
|
+
"typebox": "*"
|
|
33
|
+
},
|
|
34
|
+
"pi": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./index.ts"
|
|
37
|
+
],
|
|
38
|
+
"skills": [],
|
|
39
|
+
"prompts": [],
|
|
40
|
+
"themes": []
|
|
41
|
+
}
|
|
42
|
+
}
|