@pi-unipi/unipi 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/package.json +24 -23
- package/packages/ask-user/ask-ui.ts +45 -31
- package/packages/ask-user/launcher-ui.ts +26 -22
- package/packages/ask-user/package.json +3 -3
- package/packages/ask-user/settings-tui.ts +22 -9
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +5 -0
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/compactor/package.json +4 -4
- package/packages/compactor/src/tui/settings-overlay.ts +2 -1
- package/packages/core/constants.ts +18 -0
- package/packages/core/index.ts +1 -0
- package/packages/core/package.json +1 -1
- package/packages/core/tui-width.ts +127 -0
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/tui/settings-tui.ts +2 -1
- package/packages/image/README.md +82 -0
- package/packages/image/index.ts +1 -0
- package/packages/image/package.json +57 -0
- package/packages/image/skills/image/SKILL.md +73 -0
- package/packages/image/src/commands.ts +17 -0
- package/packages/image/src/generate.ts +201 -0
- package/packages/image/src/image-source.ts +204 -0
- package/packages/image/src/index.ts +99 -0
- package/packages/image/src/models.ts +290 -0
- package/packages/image/src/recognize.ts +223 -0
- package/packages/image/src/settings.ts +149 -0
- package/packages/image/src/tools.ts +296 -0
- package/packages/image/src/tui/model-selector.ts +279 -0
- package/packages/image/src/tui/settings-dialog.ts +236 -0
- package/packages/info-screen/package.json +2 -2
- package/packages/info-screen/tui/info-overlay.ts +3 -2
- package/packages/input-shortcuts/package.json +3 -3
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/mcp/src/tui/add-overlay.ts +10 -7
- package/packages/mcp/src/tui/settings-overlay.ts +10 -7
- package/packages/memory/package.json +3 -3
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +4 -1
- package/packages/notify/events.ts +130 -81
- package/packages/notify/package.json +3 -3
- package/packages/notify/permission-prompt-message.ts +95 -0
- package/packages/notify/settings.ts +2 -0
- package/packages/notify/skills/configure-notify/SKILL.md +31 -2
- package/packages/notify/src/__tests__/event-bus.test.ts +3 -2
- package/packages/notify/src/__tests__/permission-prompt-message.test.ts +172 -0
- package/packages/notify/tui/gotify-setup.ts +2 -1
- package/packages/notify/tui/ntfy-setup.ts +2 -1
- package/packages/notify/tui/recap-model-selector.ts +2 -2
- package/packages/notify/tui/settings-overlay.ts +2 -1
- package/packages/notify/tui/telegram-setup.ts +2 -1
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +5 -5
- package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +185 -0
- package/packages/subagents/src/index.ts +52 -10
- package/packages/unipi/index.ts +2 -0
- package/packages/updater/package.json +3 -3
- package/packages/updater/src/tui/changelog-overlay.ts +2 -2
- package/packages/updater/src/tui/readme-overlay.ts +2 -1
- package/packages/updater/src/tui/update-overlay.ts +2 -1
- package/packages/utility/package.json +2 -2
- package/packages/utility/src/tui/badge-settings-tui.ts +2 -2
- package/packages/utility/src/tui/util-settings-tui.ts +2 -2
- package/packages/web-api/README.md +28 -11
- package/packages/web-api/package.json +8 -2
- package/packages/web-api/skills/web/SKILL.md +36 -9
- package/packages/web-api/src/index.ts +15 -0
- package/packages/web-api/src/providers/duckduckgo.ts +71 -27
- package/packages/web-api/src/providers/firecrawl.ts +1 -1
- package/packages/web-api/src/providers/jina-reader.ts +1 -1
- package/packages/web-api/src/providers/jina-search.ts +1 -1
- package/packages/web-api/src/providers/perplexity.ts +2 -2
- package/packages/web-api/src/providers/serpapi.ts +1 -1
- package/packages/web-api/src/providers/tavily.ts +1 -1
- package/packages/web-api/src/providers/wigolo-client.ts +196 -0
- package/packages/web-api/src/providers/wigolo.ts +117 -0
- package/packages/web-api/src/settings.ts +1 -0
- package/packages/web-api/src/tools.ts +93 -36
- package/packages/workflow/package.json +2 -2
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/image — Model selector overlay
|
|
3
|
+
*
|
|
4
|
+
* Picks either an image-generation model (from pi-ai's image catalog) or a
|
|
5
|
+
* vision model (from pi's chat registry, filtered to image-capable models).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
9
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { boxInnerWidth, safeRepeat } from "@pi-unipi/core";
|
|
12
|
+
|
|
13
|
+
export interface SelectableModel {
|
|
14
|
+
provider: string;
|
|
15
|
+
id: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ModelSelectorKind = "generate" | "recognize";
|
|
20
|
+
|
|
21
|
+
/** Overlay listing models with filter, navigation and selection. */
|
|
22
|
+
export class ImageModelSelectorOverlay implements Component {
|
|
23
|
+
private models: SelectableModel[] = [];
|
|
24
|
+
private filtered: SelectableModel[] = [];
|
|
25
|
+
private selectedIndex = 0;
|
|
26
|
+
private filter = "";
|
|
27
|
+
private filterMode = false;
|
|
28
|
+
private saved = false;
|
|
29
|
+
private error: string | null = null;
|
|
30
|
+
private theme: Theme | null = null;
|
|
31
|
+
|
|
32
|
+
onClose?: () => void;
|
|
33
|
+
onSelect?: (modelRef: string) => void;
|
|
34
|
+
requestRender?: () => void;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly kind: ModelSelectorKind,
|
|
38
|
+
models: SelectableModel[],
|
|
39
|
+
currentRef?: string,
|
|
40
|
+
) {
|
|
41
|
+
this.models = models;
|
|
42
|
+
this.applyFilter();
|
|
43
|
+
|
|
44
|
+
if (currentRef) {
|
|
45
|
+
const index = this.filtered.findIndex(
|
|
46
|
+
(m) => `${m.provider}/${m.id}` === currentRef,
|
|
47
|
+
);
|
|
48
|
+
if (index >= 0) this.selectedIndex = index;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setTheme(theme: Theme): void {
|
|
53
|
+
this.theme = theme;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
invalidate(): void {}
|
|
57
|
+
|
|
58
|
+
handleInput(data: string): void {
|
|
59
|
+
if (this.filterMode) {
|
|
60
|
+
this.handleFilterInput(data);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
switch (data) {
|
|
65
|
+
case "\x1b[A":
|
|
66
|
+
case "k":
|
|
67
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
68
|
+
break;
|
|
69
|
+
case "\x1b[B":
|
|
70
|
+
case "j":
|
|
71
|
+
this.selectedIndex = Math.min(this.filtered.length - 1, this.selectedIndex + 1);
|
|
72
|
+
break;
|
|
73
|
+
case "/":
|
|
74
|
+
this.filterMode = true;
|
|
75
|
+
this.filter = "";
|
|
76
|
+
break;
|
|
77
|
+
case "\r":
|
|
78
|
+
this.commit();
|
|
79
|
+
break;
|
|
80
|
+
case "\x1b":
|
|
81
|
+
this.onClose?.();
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private handleFilterInput(data: string): void {
|
|
87
|
+
if (data === "\r") {
|
|
88
|
+
this.filterMode = false;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (data === "\x1b") {
|
|
92
|
+
this.filter = "";
|
|
93
|
+
this.filterMode = false;
|
|
94
|
+
this.applyFilter();
|
|
95
|
+
this.selectedIndex = 0;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (data === "\x7f" || data === "\b") {
|
|
99
|
+
this.filter = this.filter.slice(0, -1);
|
|
100
|
+
this.applyFilter();
|
|
101
|
+
this.clampSelection();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (data.length === 1 && data >= " ") {
|
|
105
|
+
this.filter += data;
|
|
106
|
+
this.applyFilter();
|
|
107
|
+
this.clampSelection();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private clampSelection(): void {
|
|
112
|
+
this.selectedIndex = Math.min(
|
|
113
|
+
this.selectedIndex,
|
|
114
|
+
Math.max(0, this.filtered.length - 1),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private applyFilter(): void {
|
|
119
|
+
const query = this.filter.toLowerCase();
|
|
120
|
+
this.filtered = query
|
|
121
|
+
? this.models.filter(
|
|
122
|
+
(m) =>
|
|
123
|
+
m.id.toLowerCase().includes(query) ||
|
|
124
|
+
m.provider.toLowerCase().includes(query) ||
|
|
125
|
+
(m.name?.toLowerCase().includes(query) ?? false),
|
|
126
|
+
)
|
|
127
|
+
: [...this.models];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private commit(): void {
|
|
131
|
+
const model = this.filtered[this.selectedIndex];
|
|
132
|
+
if (!model) {
|
|
133
|
+
this.error = "No model selected";
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.onSelect?.(`${model.provider}/${model.id}`);
|
|
138
|
+
this.saved = true;
|
|
139
|
+
this.error = null;
|
|
140
|
+
setTimeout(() => this.onClose?.(), 500);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Theme helpers ───────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
private fg(color: string, text: string): string {
|
|
146
|
+
if (this.theme) return this.theme.fg(color as never, text);
|
|
147
|
+
const codes: Record<string, string> = {
|
|
148
|
+
accent: "\x1b[36m",
|
|
149
|
+
success: "\x1b[32m",
|
|
150
|
+
warning: "\x1b[33m",
|
|
151
|
+
error: "\x1b[31m",
|
|
152
|
+
dim: "\x1b[2m",
|
|
153
|
+
borderMuted: "\x1b[90m",
|
|
154
|
+
};
|
|
155
|
+
return `${codes[color] ?? ""}${text}\x1b[0m`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private bold(text: string): string {
|
|
159
|
+
return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private frameLine(content: string, innerWidth: number): string {
|
|
163
|
+
const truncated = truncateToWidth(content, innerWidth, "");
|
|
164
|
+
const padding = safeRepeat(" ", innerWidth - visibleWidth(truncated));
|
|
165
|
+
return `${this.fg("borderMuted", "│")}${truncated}${padding}${this.fg("borderMuted", "│")}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private ruleLine(innerWidth: number): string {
|
|
169
|
+
return this.fg("borderMuted", `├${safeRepeat("─", innerWidth)}┤`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
173
|
+
const left = edge === "top" ? "┌" : "└";
|
|
174
|
+
const right = edge === "top" ? "┐" : "┘";
|
|
175
|
+
return this.fg("borderMuted", `${left}${safeRepeat("─", innerWidth)}${right}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
render(width: number): string[] {
|
|
179
|
+
const innerWidth = boxInnerWidth(width);
|
|
180
|
+
const lines: string[] = [];
|
|
181
|
+
|
|
182
|
+
const title =
|
|
183
|
+
this.kind === "generate"
|
|
184
|
+
? "🎨 Image Generation Model"
|
|
185
|
+
: "👁 Image Recognition Model";
|
|
186
|
+
const subtitle =
|
|
187
|
+
this.kind === "generate"
|
|
188
|
+
? "Model used by image_generate"
|
|
189
|
+
: "Vision model used by image_recognize";
|
|
190
|
+
|
|
191
|
+
lines.push(this.borderLine(innerWidth, "top"));
|
|
192
|
+
lines.push(this.frameLine(this.fg("accent", this.bold(title)), innerWidth));
|
|
193
|
+
lines.push(this.frameLine(this.fg("dim", subtitle), innerWidth));
|
|
194
|
+
lines.push(this.ruleLine(innerWidth));
|
|
195
|
+
|
|
196
|
+
// Filter bar
|
|
197
|
+
if (this.filterMode) {
|
|
198
|
+
lines.push(
|
|
199
|
+
this.frameLine(
|
|
200
|
+
` ${this.fg("accent", "Filter:")} ${this.filter}${this.fg("accent", "█")}`,
|
|
201
|
+
innerWidth,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
} else if (this.filter) {
|
|
205
|
+
lines.push(
|
|
206
|
+
this.frameLine(
|
|
207
|
+
` ${this.fg("dim", "Filter:")} ${this.filter} ${this.fg("dim", "(press / to edit)")}`,
|
|
208
|
+
innerWidth,
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
lines.push(
|
|
213
|
+
this.frameLine(
|
|
214
|
+
` ${this.fg("dim", `${this.models.length} models · press / to filter`)}`,
|
|
215
|
+
innerWidth,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
lines.push(this.ruleLine(innerWidth));
|
|
220
|
+
|
|
221
|
+
// Model list
|
|
222
|
+
const terminalRows = process.stdout.rows ?? 30;
|
|
223
|
+
const maxVisible = Math.max(5, terminalRows - 14);
|
|
224
|
+
const start = Math.max(0, this.selectedIndex - Math.floor(maxVisible / 2));
|
|
225
|
+
const end = Math.min(this.filtered.length, start + maxVisible);
|
|
226
|
+
|
|
227
|
+
if (this.filtered.length === 0) {
|
|
228
|
+
const empty =
|
|
229
|
+
this.models.length === 0
|
|
230
|
+
? this.kind === "generate"
|
|
231
|
+
? "No image models available (needs OpenRouter)"
|
|
232
|
+
: "No vision-capable models configured"
|
|
233
|
+
: "No models match the filter";
|
|
234
|
+
lines.push(this.frameLine(` ${this.fg("dim", empty)}`, innerWidth));
|
|
235
|
+
} else {
|
|
236
|
+
for (let i = start; i < end; i++) {
|
|
237
|
+
const model = this.filtered[i];
|
|
238
|
+
const isSelected = i === this.selectedIndex;
|
|
239
|
+
const marker = isSelected ? this.fg("accent", "▸") : " ";
|
|
240
|
+
const label = model.name || model.id;
|
|
241
|
+
const providerTag = this.fg("dim", `[${model.provider}]`);
|
|
242
|
+
const display = isSelected
|
|
243
|
+
? `${providerTag} ${this.bold(label)}`
|
|
244
|
+
: `${providerTag} ${this.fg("dim", label)}`;
|
|
245
|
+
lines.push(this.frameLine(` ${marker} ${display}`, innerWidth));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (this.filtered.length > maxVisible) {
|
|
250
|
+
const pct = Math.round(((this.selectedIndex + 1) / this.filtered.length) * 100);
|
|
251
|
+
lines.push(
|
|
252
|
+
this.frameLine(
|
|
253
|
+
this.fg("dim", ` ${pct}% (${this.selectedIndex + 1}/${this.filtered.length})`),
|
|
254
|
+
innerWidth,
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (this.error) {
|
|
260
|
+
lines.push(this.ruleLine(innerWidth));
|
|
261
|
+
lines.push(this.frameLine(` ${this.fg("error", `⚠ ${this.error}`)}`, innerWidth));
|
|
262
|
+
}
|
|
263
|
+
if (this.saved) {
|
|
264
|
+
lines.push(this.ruleLine(innerWidth));
|
|
265
|
+
lines.push(this.frameLine(` ${this.fg("success", "✓ Model saved")}`, innerWidth));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
lines.push(this.ruleLine(innerWidth));
|
|
269
|
+
lines.push(
|
|
270
|
+
this.frameLine(
|
|
271
|
+
this.fg("dim", "↑↓ navigate · / filter · Enter select · Esc cancel"),
|
|
272
|
+
innerWidth,
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
276
|
+
|
|
277
|
+
return lines;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/image — Settings dialog
|
|
3
|
+
*
|
|
4
|
+
* Simple `ctx.ui.select` loop, matching the web-api settings pattern. The
|
|
5
|
+
* model pickers are richer overlays (see model-selector.ts).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_CONFIG,
|
|
12
|
+
DEFAULT_RECOGNIZE_SYSTEM_PROMPT,
|
|
13
|
+
getOutputDir,
|
|
14
|
+
loadConfig,
|
|
15
|
+
saveConfig,
|
|
16
|
+
type ImageConfig,
|
|
17
|
+
} from "../settings.js";
|
|
18
|
+
import {
|
|
19
|
+
formatModelRef,
|
|
20
|
+
listImageGenModels,
|
|
21
|
+
listVisionModels,
|
|
22
|
+
type ChatModelRegistry,
|
|
23
|
+
} from "../models.js";
|
|
24
|
+
import { ImageModelSelectorOverlay, type SelectableModel } from "./model-selector.js";
|
|
25
|
+
|
|
26
|
+
const EXIT = "__exit__";
|
|
27
|
+
|
|
28
|
+
function describe(config: ImageConfig): Array<{ value: string; label: string }> {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
value: "__gen_model__",
|
|
32
|
+
label: `🎨 Generation model — ${config.generate.model || "(none)"}`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: "__gen_enabled__",
|
|
36
|
+
label: ` image_generate tool — ${config.generate.enabled ? "enabled" : "disabled"}`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: "__gen_save__",
|
|
40
|
+
label: ` Save to disk — ${config.generate.saveToDisk ? "on" : "off"}`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: "__gen_dir__",
|
|
44
|
+
label: ` Output directory — ${config.generate.outputDir}`,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: "__rec_model__",
|
|
48
|
+
label: `👁 Recognition model — ${config.recognize.model || "(session model)"}`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: "__rec_enabled__",
|
|
52
|
+
label: ` image_recognize tool — ${config.recognize.enabled ? "enabled" : "disabled"}`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
value: "__rec_prompt__",
|
|
56
|
+
label: ` System prompt — ${summarize(config.recognize.systemPrompt)}`,
|
|
57
|
+
},
|
|
58
|
+
{ value: "__reset__", label: "↺ Reset to defaults" },
|
|
59
|
+
{ value: EXIT, label: "← Back" },
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function summarize(text: string, max = 45): string {
|
|
64
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
65
|
+
if (!flat) return "(empty)";
|
|
66
|
+
return flat.length <= max ? flat : `${flat.slice(0, max)}…`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Show the settings dialog until the user backs out. */
|
|
70
|
+
export async function showSettingsDialog(ctx: ExtensionCommandContext): Promise<void> {
|
|
71
|
+
for (;;) {
|
|
72
|
+
const config = loadConfig();
|
|
73
|
+
const options = describe(config);
|
|
74
|
+
const labels = options.map((o) => o.label);
|
|
75
|
+
|
|
76
|
+
const picked = await ctx.ui.select("Image Settings", labels);
|
|
77
|
+
if (!picked) return;
|
|
78
|
+
|
|
79
|
+
const choice = options.find((o) => o.label === picked)?.value;
|
|
80
|
+
if (!choice || choice === EXIT) return;
|
|
81
|
+
|
|
82
|
+
await applyChoice(ctx, choice, config);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function applyChoice(
|
|
87
|
+
ctx: ExtensionCommandContext,
|
|
88
|
+
choice: string,
|
|
89
|
+
config: ImageConfig,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
switch (choice) {
|
|
92
|
+
case "__gen_enabled__":
|
|
93
|
+
config.generate.enabled = !config.generate.enabled;
|
|
94
|
+
saveConfig(config);
|
|
95
|
+
ctx.ui.notify(
|
|
96
|
+
`image_generate ${config.generate.enabled ? "enabled" : "disabled"} — restart the session to apply.`,
|
|
97
|
+
"info",
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
|
|
101
|
+
case "__rec_enabled__":
|
|
102
|
+
config.recognize.enabled = !config.recognize.enabled;
|
|
103
|
+
saveConfig(config);
|
|
104
|
+
ctx.ui.notify(
|
|
105
|
+
`image_recognize ${config.recognize.enabled ? "enabled" : "disabled"} — restart the session to apply.`,
|
|
106
|
+
"info",
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
|
|
110
|
+
case "__gen_save__":
|
|
111
|
+
config.generate.saveToDisk = !config.generate.saveToDisk;
|
|
112
|
+
saveConfig(config);
|
|
113
|
+
ctx.ui.notify(
|
|
114
|
+
config.generate.saveToDisk
|
|
115
|
+
? `Generated images will be saved to ${getOutputDir(config)}`
|
|
116
|
+
: "Generated images will be returned inline only.",
|
|
117
|
+
"info",
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
|
|
121
|
+
case "__gen_dir__": {
|
|
122
|
+
const value = await ctx.ui.input(
|
|
123
|
+
"Output directory for generated images",
|
|
124
|
+
config.generate.outputDir,
|
|
125
|
+
);
|
|
126
|
+
if (value?.trim()) {
|
|
127
|
+
config.generate.outputDir = value.trim();
|
|
128
|
+
saveConfig(config);
|
|
129
|
+
ctx.ui.notify(`Output directory set to ${getOutputDir(config)}`, "info");
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case "__rec_prompt__": {
|
|
135
|
+
const value = await ctx.ui.input(
|
|
136
|
+
"System prompt for image recognition (blank to restore the default)",
|
|
137
|
+
config.recognize.systemPrompt,
|
|
138
|
+
);
|
|
139
|
+
if (value === undefined) return;
|
|
140
|
+
config.recognize.systemPrompt = value.trim() || DEFAULT_RECOGNIZE_SYSTEM_PROMPT;
|
|
141
|
+
saveConfig(config);
|
|
142
|
+
ctx.ui.notify("System prompt updated.", "info");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case "__gen_model__":
|
|
147
|
+
await pickModel(ctx, "generate", config);
|
|
148
|
+
return;
|
|
149
|
+
|
|
150
|
+
case "__rec_model__":
|
|
151
|
+
await pickModel(ctx, "recognize", config);
|
|
152
|
+
return;
|
|
153
|
+
|
|
154
|
+
case "__reset__":
|
|
155
|
+
saveConfig(structuredClone(DEFAULT_CONFIG));
|
|
156
|
+
ctx.ui.notify("Image settings reset to defaults.", "info");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function pickModel(
|
|
162
|
+
ctx: ExtensionCommandContext,
|
|
163
|
+
kind: "generate" | "recognize",
|
|
164
|
+
config: ImageConfig,
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
const models = await collectModels(ctx, kind);
|
|
167
|
+
|
|
168
|
+
if (models.length === 0) {
|
|
169
|
+
ctx.ui.notify(
|
|
170
|
+
kind === "generate"
|
|
171
|
+
? "No image models available. Image generation is served through OpenRouter — add a key with /login."
|
|
172
|
+
: "No vision-capable models configured. Add a model that accepts image input.",
|
|
173
|
+
"warning",
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!ctx.hasUI) {
|
|
179
|
+
ctx.ui.notify("Model selection requires an interactive UI.", "warning");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const current =
|
|
184
|
+
kind === "generate" ? config.generate.model : config.recognize.model;
|
|
185
|
+
|
|
186
|
+
await new Promise<void>((resolve) => {
|
|
187
|
+
ctx.ui.custom(
|
|
188
|
+
(tui, theme, _keybindings, done) => {
|
|
189
|
+
const overlay = new ImageModelSelectorOverlay(kind, models, current);
|
|
190
|
+
overlay.setTheme(theme);
|
|
191
|
+
overlay.onSelect = (modelRef) => {
|
|
192
|
+
const next = loadConfig();
|
|
193
|
+
if (kind === "generate") next.generate.model = modelRef;
|
|
194
|
+
else next.recognize.model = modelRef;
|
|
195
|
+
saveConfig(next);
|
|
196
|
+
};
|
|
197
|
+
overlay.onClose = () => done(undefined);
|
|
198
|
+
return {
|
|
199
|
+
render: (width: number) => overlay.render(width),
|
|
200
|
+
invalidate: () => overlay.invalidate(),
|
|
201
|
+
handleInput: (data: string) => {
|
|
202
|
+
overlay.handleInput(data);
|
|
203
|
+
tui.requestRender();
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
overlay: true,
|
|
209
|
+
overlayOptions: { width: "80%", minWidth: 50, anchor: "center", margin: 2 },
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
resolve();
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function collectModels(
|
|
217
|
+
ctx: ExtensionCommandContext,
|
|
218
|
+
kind: "generate" | "recognize",
|
|
219
|
+
): Promise<SelectableModel[]> {
|
|
220
|
+
if (kind === "generate") {
|
|
221
|
+
const models = await listImageGenModels();
|
|
222
|
+
return models.map((m) => ({ provider: m.provider, id: m.id, name: m.name }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const registry = (ctx as unknown as { modelRegistry?: ChatModelRegistry })
|
|
226
|
+
.modelRegistry;
|
|
227
|
+
if (!registry) return [];
|
|
228
|
+
|
|
229
|
+
return listVisionModels(registry).map((m) => ({
|
|
230
|
+
provider: m.provider,
|
|
231
|
+
id: m.id,
|
|
232
|
+
name: m.name,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export { formatModelRef };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/info-screen",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.
|
|
36
|
+
"@pi-unipi/core": "2.2.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -13,6 +13,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
13
13
|
import { infoRegistry } from "../registry.js";
|
|
14
14
|
import { getInfoSettings } from "../config.js";
|
|
15
15
|
import type { InfoGroup, GroupData } from "../types.js";
|
|
16
|
+
import { boxInnerWidth } from "@pi-unipi/core";
|
|
16
17
|
|
|
17
18
|
/** Tab color palette */
|
|
18
19
|
const TAB_FG: Array<"accent" | "success" | "warning" | "error"> = [
|
|
@@ -275,7 +276,7 @@ export class InfoOverlay implements Component {
|
|
|
275
276
|
// ─── State views ─────────────────────────────────────────────────────
|
|
276
277
|
|
|
277
278
|
private renderEmpty(width: number): string[] {
|
|
278
|
-
const innerWidth =
|
|
279
|
+
const innerWidth = boxInnerWidth(width);
|
|
279
280
|
const lines: string[] = [];
|
|
280
281
|
lines.push(this.borderLine(innerWidth, "top"));
|
|
281
282
|
lines.push(this.frameLine(this.fg("accent", this.bold("📊 UniPi Info Screen")), innerWidth));
|
|
@@ -292,7 +293,7 @@ export class InfoOverlay implements Component {
|
|
|
292
293
|
// ─── Dashboard ───────────────────────────────────────────────────────
|
|
293
294
|
|
|
294
295
|
private renderDashboard(width: number): string[] {
|
|
295
|
-
const innerWidth =
|
|
296
|
+
const innerWidth = boxInnerWidth(width);
|
|
296
297
|
const group = this.groups[this.activeTabIndex];
|
|
297
298
|
const data = this.groupData.get(group.id) ?? {};
|
|
298
299
|
const isLoading = this.groupLoading.get(group.id) ?? false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/input-shortcuts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "
|
|
30
|
+
"test": "npx tsx --test tests/**/*.test.ts"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.
|
|
36
|
+
"@pi-unipi/core": "2.2.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/kanboard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Visualization layer for unipi workflow — HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@pi-unipi/core": "2.
|
|
42
|
+
"@pi-unipi/core": "2.2.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "MCP server management extension for Pi coding agent — browse, add, configure, and use MCP servers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@pi-unipi/core": "2.
|
|
30
|
+
"@pi-unipi/core": "2.2.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
getProjectConfigDir,
|
|
32
32
|
} from "../config/manager.js";
|
|
33
33
|
import { validateMcpConfig, DEFAULT_MCP_CONFIG, DEFAULT_METADATA } from "../config/schema.js";
|
|
34
|
+
import { boxInnerWidth, normalizeWidth, WidthKeyedCache } from "@pi-unipi/core";
|
|
34
35
|
|
|
35
36
|
type Mode = "normal" | "search" | "editor";
|
|
36
37
|
type StatusKind = "info" | "success" | "warn" | "error";
|
|
@@ -158,11 +159,12 @@ export function renderMcpAddOverlay(params?: {
|
|
|
158
159
|
// Ctrl+S handles saving; no submit path needed.
|
|
159
160
|
(editor as any).disableSubmit = true;
|
|
160
161
|
|
|
161
|
-
|
|
162
|
+
// Width-keyed so a terminal resize can never serve stale, over-wide lines.
|
|
163
|
+
const lineCache = new WidthKeyedCache();
|
|
162
164
|
let lastListHeight = 12;
|
|
163
165
|
|
|
164
166
|
function refresh() {
|
|
165
|
-
|
|
167
|
+
lineCache.clear();
|
|
166
168
|
tui.requestRender();
|
|
167
169
|
}
|
|
168
170
|
|
|
@@ -537,11 +539,13 @@ export function renderMcpAddOverlay(params?: {
|
|
|
537
539
|
|
|
538
540
|
// ─── Rendering ────────────────────────────────────────────────────
|
|
539
541
|
|
|
540
|
-
function render(
|
|
541
|
-
|
|
542
|
+
function render(rawWidth: number): string[] {
|
|
543
|
+
const width = normalizeWidth(rawWidth);
|
|
544
|
+
const cached = lineCache.get(width);
|
|
545
|
+
if (cached) return cached;
|
|
542
546
|
|
|
543
547
|
const lines: string[] = [];
|
|
544
|
-
const innerWidth =
|
|
548
|
+
const innerWidth = boxInnerWidth(width);
|
|
545
549
|
|
|
546
550
|
// Pane widths: account for left │, middle │, right │ borders
|
|
547
551
|
const leftW = Math.floor((innerWidth - 1) / 2);
|
|
@@ -776,8 +780,7 @@ export function renderMcpAddOverlay(params?: {
|
|
|
776
780
|
);
|
|
777
781
|
lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
|
|
778
782
|
|
|
779
|
-
|
|
780
|
-
return lines;
|
|
783
|
+
return lineCache.set(width, lines);
|
|
781
784
|
}
|
|
782
785
|
|
|
783
786
|
return { render, invalidate: refresh, handleInput };
|