@pi-unipi/utility 2.6.1 → 2.6.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.
@@ -1,388 +0,0 @@
1
- /**
2
- * @pi-unipi/utility — Badge Settings TUI Overlay
3
- *
4
- * Interactive settings overlay for badge configuration.
5
- * Three settings: auto-generate toggle, badge-enabled toggle, generation model selector.
6
- * Model list loaded from shared model cache (~/.unipi/config/models-cache.json).
7
- */
8
-
9
- import type { Component } from "@earendil-works/pi-tui";
10
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
- import type { CachedModel } from "@pi-unipi/core";
12
- import { readModelCache, boxInnerWidth } from "@pi-unipi/core";
13
- import {
14
- readBadgeSettings,
15
- writeBadgeSettings,
16
- type BadgeSettings,
17
- } from "./badge-settings.js";
18
-
19
- /** ANSI escape codes */
20
- const ansi = {
21
- reset: "\x1b[0m",
22
- bold: "\x1b[1m",
23
- dim: "\x1b[2m",
24
- cyan: "\x1b[36m",
25
- green: "\x1b[32m",
26
- yellow: "\x1b[33m",
27
- red: "\x1b[31m",
28
- gray: "\x1b[90m",
29
- white: "\x1b[37m",
30
- };
31
-
32
- /** Toggle symbols */
33
- const TOGGLE_ON = `${ansi.green}●${ansi.reset}`;
34
- const TOGGLE_OFF = `${ansi.dim}○${ansi.reset}`;
35
-
36
- /** Active mode */
37
- type Mode = "settings" | "model-picker";
38
-
39
- /** Setting row types */
40
- interface BooleanSetting {
41
- type: "boolean";
42
- key: keyof BadgeSettings;
43
- label: string;
44
- description: string;
45
- getValue: (s: BadgeSettings) => boolean;
46
- }
47
-
48
- interface ModelSetting {
49
- type: "model";
50
- key: "generationModel";
51
- label: string;
52
- description: string;
53
- }
54
-
55
- type SettingItem = BooleanSetting | ModelSetting;
56
-
57
- /** Settings list */
58
- const SETTINGS: SettingItem[] = [
59
- {
60
- type: "boolean",
61
- key: "autoGen",
62
- label: "Auto generate",
63
- description: "Generate session name on first user message",
64
- getValue: (s) => s.autoGen,
65
- },
66
- {
67
- type: "boolean",
68
- key: "badgeEnabled",
69
- label: "Badge enabled",
70
- description: "Show the name badge overlay",
71
- getValue: (s) => s.badgeEnabled,
72
- },
73
- {
74
- type: "model",
75
- key: "generationModel",
76
- label: "Generation model",
77
- description: "Model to use for badge name generation",
78
- },
79
- ];
80
-
81
- /**
82
- * Badge Settings TUI overlay.
83
- * Implements the Component interface for use with ctx.ui.custom().
84
- */
85
- export class BadgeSettingsTui implements Component {
86
- private settings: BadgeSettings;
87
- private mode: Mode = "settings";
88
- private selectedIndex = 0;
89
- private modelScrollOffset = 0;
90
- private models: CachedModel[] = [];
91
-
92
- /** Theme reference for rendering (set externally) */
93
- private _theme: any = null;
94
-
95
- /** Callback when overlay should close */
96
- onClose?: () => void;
97
-
98
- /** Callback to request a re-render */
99
- requestRender?: () => void;
100
-
101
- constructor() {
102
- this.settings = readBadgeSettings();
103
- this.models = readModelCache();
104
- }
105
-
106
- /**
107
- * Set the pi-tui theme for styled rendering.
108
- */
109
- setTheme(theme: any): void {
110
- this._theme = theme;
111
- }
112
-
113
- /**
114
- * Invalidate cached render state.
115
- */
116
- invalidate(): void {
117
- // No cached state to invalidate
118
- }
119
-
120
- /**
121
- * Handle keyboard input.
122
- */
123
- handleInput(data: string): void {
124
- if (this.mode === "settings") {
125
- this.handleSettingsInput(data);
126
- } else {
127
- this.handleModelPickerInput(data);
128
- }
129
- }
130
-
131
- /**
132
- * Handle input in settings mode.
133
- */
134
- private handleSettingsInput(data: string): void {
135
- switch (data) {
136
- case "\x1b[A": // Up arrow
137
- case "k":
138
- this.selectedIndex =
139
- (this.selectedIndex - 1 + SETTINGS.length) % SETTINGS.length;
140
- break;
141
- case "\x1b[B": // Down arrow
142
- case "j":
143
- this.selectedIndex = (this.selectedIndex + 1) % SETTINGS.length;
144
- break;
145
- case " ": // Space — toggle boolean settings
146
- this.toggleCurrentSetting();
147
- break;
148
- case "\r": // Enter — open model picker or toggle
149
- if (SETTINGS[this.selectedIndex].type === "model") {
150
- this.enterModelPicker();
151
- } else {
152
- this.toggleCurrentSetting();
153
- }
154
- break;
155
- case "\x1b": // Escape — close and save
156
- this.save();
157
- this.onClose?.();
158
- break;
159
- }
160
- }
161
-
162
- /**
163
- * Handle input in model picker mode.
164
- */
165
- private handleModelPickerInput(data: string): void {
166
- const allModels = this.getModelList();
167
-
168
- switch (data) {
169
- case "\x1b[A": // Up arrow
170
- case "k":
171
- this.selectedIndex =
172
- (this.selectedIndex - 1 + allModels.length) % allModels.length;
173
- this.adjustModelScroll(allModels.length);
174
- break;
175
- case "\x1b[B": // Down arrow
176
- case "j":
177
- this.selectedIndex = (this.selectedIndex + 1) % allModels.length;
178
- this.adjustModelScroll(allModels.length);
179
- break;
180
- case "\r": // Enter — select model
181
- this.selectModel();
182
- break;
183
- case "\x1b": // Escape — cancel picker
184
- this.mode = "settings";
185
- this.selectedIndex = SETTINGS.findIndex(
186
- (s) => s.key === "generationModel",
187
- );
188
- break;
189
- }
190
- }
191
-
192
- /**
193
- * Toggle the currently selected boolean setting.
194
- */
195
- private toggleCurrentSetting(): void {
196
- const item = SETTINGS[this.selectedIndex];
197
- if (item.type !== "boolean") return;
198
-
199
- const current = item.getValue(this.settings);
200
- (this.settings as any)[item.key] = !current;
201
- this.save();
202
- }
203
-
204
- /**
205
- * Enter model picker mode.
206
- */
207
- private enterModelPicker(): void {
208
- this.mode = "model-picker";
209
- const allModels = this.getModelList();
210
- const currentModel = this.settings.generationModel;
211
- this.selectedIndex = allModels.findIndex((m) => m.id === currentModel);
212
- if (this.selectedIndex < 0) this.selectedIndex = 0;
213
- this.modelScrollOffset = 0;
214
- this.adjustModelScroll(allModels.length);
215
- }
216
-
217
- /**
218
- * Select the current model in the picker.
219
- */
220
- private selectModel(): void {
221
- const allModels = this.getModelList();
222
- const selected = allModels[this.selectedIndex];
223
- if (selected) {
224
- this.settings.generationModel = selected.id;
225
- this.save();
226
- }
227
- this.mode = "settings";
228
- this.selectedIndex = SETTINGS.findIndex((s) => s.key === "generationModel");
229
- }
230
-
231
- /**
232
- * Get model list with "inherit" as first entry.
233
- */
234
- private getModelList(): Array<{ id: string; label: string }> {
235
- const list: Array<{ id: string; label: string }> = [
236
- { id: "inherit", label: "inherit (use parent model)" },
237
- ];
238
- for (const m of this.models) {
239
- const fullId = `${m.provider}/${m.id}`;
240
- list.push({
241
- id: fullId,
242
- label: m.name ? `${fullId} (${m.name})` : fullId,
243
- });
244
- }
245
- return list;
246
- }
247
-
248
- /**
249
- * Adjust scroll offset so the selected item is visible.
250
- */
251
- private adjustModelScroll(totalItems: number): void {
252
- // Reserve ~10 lines for visible model list
253
- const visibleLines = 10;
254
- if (this.selectedIndex < this.modelScrollOffset) {
255
- this.modelScrollOffset = this.selectedIndex;
256
- } else if (this.selectedIndex >= this.modelScrollOffset + visibleLines) {
257
- this.modelScrollOffset = this.selectedIndex - visibleLines + 1;
258
- }
259
- // Clamp
260
- this.modelScrollOffset = Math.max(
261
- 0,
262
- Math.min(this.modelScrollOffset, totalItems - visibleLines),
263
- );
264
- }
265
-
266
- /**
267
- * Save settings to disk.
268
- */
269
- private save(): void {
270
- writeBadgeSettings(this.settings);
271
- }
272
-
273
- /**
274
- * Render the overlay.
275
- */
276
- render(width: number): string[] {
277
- const lines: string[] = [];
278
- const innerWidth = boxInnerWidth(width);
279
-
280
- const padVisible = (content: string, targetWidth: number): string => {
281
- const vw = visibleWidth(content);
282
- const pad = Math.max(0, targetWidth - vw);
283
- return content + " ".repeat(pad);
284
- };
285
-
286
- const add = (s: string) =>
287
- lines.push(
288
- `${ansi.cyan}│${ansi.reset}` +
289
- padVisible(truncateToWidth(s, innerWidth), innerWidth) +
290
- `${ansi.cyan}│${ansi.reset}`,
291
- );
292
-
293
- const addEmpty = () =>
294
- lines.push(
295
- `${ansi.cyan}│${ansi.reset}` +
296
- " ".repeat(innerWidth) +
297
- `${ansi.cyan}│${ansi.reset}`,
298
- );
299
-
300
- // Top border
301
- lines.push(`${ansi.cyan}╭${"─".repeat(innerWidth)}╮${ansi.reset}`);
302
-
303
- // Header
304
- add(`${ansi.bold}${ansi.cyan}Badge Settings${ansi.reset}`);
305
- add(`${ansi.dim}Configure badge generation behavior${ansi.reset}`);
306
- addEmpty();
307
-
308
- // Settings list
309
- for (let i = 0; i < SETTINGS.length; i++) {
310
- const item = SETTINGS[i];
311
- const isSelected = i === this.selectedIndex && this.mode === "settings";
312
- const selector = isSelected ? `${ansi.cyan}▸${ansi.reset}` : " ";
313
-
314
- if (item.type === "boolean") {
315
- const value = item.getValue(this.settings);
316
- const toggle = value ? TOGGLE_ON : TOGGLE_OFF;
317
- const labelColor = isSelected ? ansi.bold : ansi.dim;
318
-
319
- add(
320
- `${selector} ${toggle} ${labelColor}${item.label}${ansi.reset}`,
321
- );
322
- add(` ${ansi.gray}${item.description}${ansi.reset}`);
323
- } else if (item.type === "model") {
324
- const labelColor = isSelected ? ansi.bold : ansi.dim;
325
- const modelDisplay = this.settings.generationModel;
326
-
327
- add(
328
- `${selector} ${ansi.yellow}⚙${ansi.reset} ${labelColor}${item.label}${ansi.reset}: ${ansi.white}${modelDisplay}${ansi.reset}`,
329
- );
330
- add(` ${ansi.gray}${item.description}${ansi.reset}`);
331
- add(` ${ansi.dim}Enter to select model${ansi.reset}`);
332
- }
333
- }
334
-
335
- // Model picker (inline)
336
- if (this.mode === "model-picker") {
337
- addEmpty();
338
- add(`${ansi.bold}${ansi.cyan}── Available Models ──${ansi.reset}`);
339
-
340
- const allModels = this.getModelList();
341
- const visibleLines = 10;
342
- const start = this.modelScrollOffset;
343
- const end = Math.min(start + visibleLines, allModels.length);
344
-
345
- // Scroll indicator up
346
- if (start > 0) {
347
- add(` ${ansi.dim}▲ ${start} more above${ansi.reset}`);
348
- }
349
-
350
- for (let i = start; i < end; i++) {
351
- const m = allModels[i];
352
- const isSelected = i === this.selectedIndex;
353
- const selector = isSelected ? `${ansi.cyan}▸${ansi.reset}` : " ";
354
- const labelColor = isSelected ? ansi.bold + ansi.white : ansi.dim;
355
-
356
- add(
357
- `${selector} ${labelColor}${m.label}${ansi.reset}`,
358
- );
359
- }
360
-
361
- // Scroll indicator down
362
- if (end < allModels.length) {
363
- add(
364
- ` ${ansi.dim}▼ ${allModels.length - end} more below${ansi.reset}`,
365
- );
366
- }
367
- }
368
-
369
- // Footer
370
- addEmpty();
371
-
372
- if (this.mode === "model-picker") {
373
- add(
374
- `${ansi.dim}↑↓ navigate • Enter select • Esc cancel${ansi.reset}`,
375
- );
376
- } else {
377
- add(
378
- `${ansi.dim}↑↓ navigate • Space toggle • Enter select model • Esc close${ansi.reset}`,
379
- );
380
- add(`${ansi.dim}Config: .unipi/config/badge.json${ansi.reset}`);
381
- }
382
-
383
- // Bottom border
384
- lines.push(`${ansi.cyan}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
385
-
386
- return lines;
387
- }
388
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * @pi-unipi/utility — Badge Settings Manager
3
- *
4
- * Thin wrappers over the unified settings manager (util-settings.json).
5
- * Existing callers continue to work unchanged.
6
- */
7
-
8
- import {
9
- readUtilSettings,
10
- writeUtilSettings,
11
- type BadgeSettingsSection,
12
- } from "../diff/settings.js";
13
-
14
- /** Badge settings interface (re-exports BadgeSettingsSection for backward compat) */
15
- export type BadgeSettings = BadgeSettingsSection;
16
-
17
- /** Default badge settings for formatBadgeSettings display */
18
- const BADGE_CONFIG_FILE = ".unipi/config/util-settings.json";
19
-
20
- /**
21
- * Read badge settings from unified config.
22
- * Returns defaults if file doesn't exist or is malformed.
23
- */
24
- export function readBadgeSettings(): BadgeSettings {
25
- return readUtilSettings().badge;
26
- }
27
-
28
- /**
29
- * Write badge settings to unified config.
30
- */
31
- export function writeBadgeSettings(settings: BadgeSettings): void {
32
- const util = readUtilSettings();
33
- util.badge = settings;
34
- writeUtilSettings(util);
35
- }
36
-
37
- /**
38
- * Update a single badge setting.
39
- */
40
- export function updateBadgeSetting<K extends keyof BadgeSettings>(
41
- key: K,
42
- value: BadgeSettings[K],
43
- ): BadgeSettings {
44
- const settings = readBadgeSettings();
45
- settings[key] = value;
46
- writeBadgeSettings(settings);
47
- return settings;
48
- }
49
-
50
- /**
51
- * Format badge settings for display.
52
- */
53
- export function formatBadgeSettings(settings: BadgeSettings): string {
54
- const toggle = (v: boolean) => (v ? "✓ enabled" : "✗ disabled");
55
- return [
56
- "## Badge Settings",
57
- "",
58
- `| Setting | Status | Description |`,
59
- `|---------|--------|-------------|`,
60
- `| Auto Generate | ${toggle(settings.autoGen)} | Generate name on first message |`,
61
- `| Badge Enabled | ${toggle(settings.badgeEnabled)} | Show badge overlay |`,
62
- `| Agent Tool | ${toggle(settings.agentTool)} | Allow agents to call set_session_name |`,
63
- `| Generation Model | ${settings.generationModel} | Model for badge name generation |`,
64
- "",
65
- `Config: \`${BADGE_CONFIG_FILE}\``,
66
- ].join("\n");
67
- }