@pi-unipi/utility 2.6.0 → 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,51 +1,30 @@
1
1
  /**
2
- * @pi-unipi/utility — Unified Settings TUI Overlay
2
+ * @pi-unipi/utility — Settings TUI Overlay
3
3
  *
4
- * Single TUI overlay with two navigable sections:
5
- * - Badge: autoGen, badgeEnabled, agentTool, generationModel
6
- * - Diff Rendering: enabled, theme preset, shikiTheme
7
- *
8
- * Replaces badge-settings-tui.ts as the primary settings interface.
4
+ * Single TUI overlay for badge settings.
9
5
  */
10
6
 
11
7
  import type { Component } from "@earendil-works/pi-tui";
12
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
+ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import { ansi, TOGGLE_ON, TOGGLE_OFF } from "@pi-unipi/core";
13
10
  import type { CachedModel } from "@pi-unipi/core";
14
11
  import { readModelCache, boxInnerWidth } from "@pi-unipi/core";
15
12
  import {
16
13
  readUtilSettings,
17
14
  writeUtilSettings,
18
15
  type UtilSettings,
19
- type BadgeSettingsSection,
20
- type DiffSettings,
21
- } from "../diff/settings.js";
22
- import { getAllPresets } from "../diff/theme.js";
16
+ } from "../settings.js";
23
17
 
24
18
  /** ANSI escape codes */
25
- const ansi = {
26
- reset: "\x1b[0m",
27
- bold: "\x1b[1m",
28
- dim: "\x1b[2m",
29
- cyan: "\x1b[36m",
30
- green: "\x1b[32m",
31
- yellow: "\x1b[33m",
32
- red: "\x1b[31m",
33
- gray: "\x1b[90m",
34
- white: "\x1b[37m",
35
- blue: "\x1b[34m",
36
- };
37
19
 
38
20
  /** Toggle symbols */
39
- const TOGGLE_ON = `${ansi.green}●${ansi.reset}`;
40
- const TOGGLE_OFF = `${ansi.dim}○${ansi.reset}`;
41
21
 
42
22
  /** Active mode */
43
- type Mode = "settings" | "model-picker" | "theme-picker" | "shiki-picker";
23
+ type Mode = "settings" | "model-picker";
44
24
 
45
25
  /** Setting row types */
46
26
  interface BooleanSetting {
47
27
  type: "boolean";
48
- section: "badge" | "diff";
49
28
  key: string;
50
29
  label: string;
51
30
  description: string;
@@ -54,11 +33,10 @@ interface BooleanSetting {
54
33
 
55
34
  interface PickerSetting {
56
35
  type: "picker";
57
- section: "badge" | "diff";
58
36
  key: string;
59
37
  label: string;
60
38
  description: string;
61
- pickerType: "model" | "theme" | "shiki";
39
+ pickerType: "model";
62
40
  getValue: (s: UtilSettings) => string;
63
41
  }
64
42
 
@@ -71,11 +49,9 @@ type SettingItem = BooleanSetting | PickerSetting | SectionHeader;
71
49
 
72
50
  /** All settings items */
73
51
  const SETTINGS: SettingItem[] = [
74
- // Badge section
75
52
  { type: "section", label: "Badge" },
76
53
  {
77
54
  type: "boolean",
78
- section: "badge",
79
55
  key: "autoGen",
80
56
  label: "Auto generate",
81
57
  description: "Generate session name on first user message",
@@ -83,7 +59,6 @@ const SETTINGS: SettingItem[] = [
83
59
  },
84
60
  {
85
61
  type: "boolean",
86
- section: "badge",
87
62
  key: "badgeEnabled",
88
63
  label: "Badge enabled",
89
64
  description: "Show the name badge overlay",
@@ -91,79 +66,30 @@ const SETTINGS: SettingItem[] = [
91
66
  },
92
67
  {
93
68
  type: "boolean",
94
- section: "badge",
95
69
  key: "agentTool",
96
70
  label: "Agent tool",
97
71
  description: "Allow agents to call set_session_name",
98
72
  getValue: (s) => s.badge.agentTool,
99
73
  },
74
+ {
75
+ type: "boolean",
76
+ key: "herdrSync",
77
+ label: "Herdr sync",
78
+ description: "Sync session name to herdr tab/pane title",
79
+ getValue: (s) => s.badge.herdrSync,
80
+ },
100
81
  {
101
82
  type: "picker",
102
- section: "badge",
103
83
  key: "generationModel",
104
84
  label: "Generation model",
105
85
  description: "Model for badge name generation",
106
86
  pickerType: "model",
107
87
  getValue: (s) => s.badge.generationModel,
108
88
  },
109
- // Diff Rendering section
110
- { type: "section", label: "Diff Rendering" },
111
- {
112
- type: "boolean",
113
- section: "diff",
114
- key: "enabled",
115
- label: "Enabled",
116
- description: "Shiki-powered syntax-highlighted diffs",
117
- getValue: (s) => s.diff.enabled,
118
- },
119
- {
120
- type: "picker",
121
- section: "diff",
122
- key: "theme",
123
- label: "Theme",
124
- description: "Diff color preset",
125
- pickerType: "theme",
126
- getValue: (s) => s.diff.theme,
127
- },
128
- {
129
- type: "picker",
130
- section: "diff",
131
- key: "shikiTheme",
132
- label: "Shiki theme",
133
- description: "Syntax highlighting grammar",
134
- pickerType: "shiki",
135
- getValue: (s) => s.diff.shikiTheme,
136
- },
137
- ];
138
-
139
- /** List of known Shiki themes */
140
- const SHIKI_THEMES = [
141
- "github-dark",
142
- "github-light",
143
- "dracula",
144
- "one-dark-pro",
145
- "catppuccin-mocha",
146
- "catppuccin-latte",
147
- "nord",
148
- "tokyo-night",
149
- "tokyo-night-storm",
150
- "night-owl",
151
- "material-theme",
152
- "material-theme-palenight",
153
- "monokai",
154
- "solarized-dark",
155
- "solarized-light",
156
- "vitesse-dark",
157
- "vitesse-light",
158
- "ayu-dark",
159
- "ayu-mirage",
160
- "slack-dark",
161
- "slack-ochin",
162
89
  ];
163
90
 
164
91
  /**
165
- * Unified Settings TUI overlay.
166
- * Combines badge and diff settings in a single navigable interface.
92
+ * Settings TUI overlay for badge configuration.
167
93
  */
168
94
  export class UtilSettingsTui implements Component {
169
95
  private settings: UtilSettings;
@@ -172,10 +98,7 @@ export class UtilSettingsTui implements Component {
172
98
  private scrollOffset = 0;
173
99
  private models: CachedModel[] = [];
174
100
 
175
- /** Callback when overlay should close */
176
101
  onClose?: () => void;
177
-
178
- /** Callback to request a re-render */
179
102
  requestRender?: () => void;
180
103
 
181
104
  constructor() {
@@ -183,16 +106,8 @@ export class UtilSettingsTui implements Component {
183
106
  this.models = readModelCache();
184
107
  }
185
108
 
186
- /**
187
- * Invalidate cached render state.
188
- */
189
- invalidate(): void {
190
- // No cached state to invalidate
191
- }
109
+ invalidate(): void {}
192
110
 
193
- /**
194
- * Handle keyboard input.
195
- */
196
111
  handleInput(data: string): void {
197
112
  switch (this.mode) {
198
113
  case "settings":
@@ -201,90 +116,52 @@ export class UtilSettingsTui implements Component {
201
116
  case "model-picker":
202
117
  this.handlePickerInput(data, this.getModelList(), "model");
203
118
  break;
204
- case "theme-picker":
205
- this.handlePickerInput(data, this.getThemeList(), "theme");
206
- break;
207
- case "shiki-picker":
208
- this.handlePickerInput(data, this.getShikiList(), "shiki");
209
- break;
210
119
  }
211
120
  }
212
121
 
213
- /**
214
- * Handle input in settings mode.
215
- */
216
122
  private handleSettingsInput(data: string): void {
217
- // Get navigable items (skip section headers)
218
123
  const navItems = SETTINGS.filter((s) => s.type !== "section");
219
124
 
220
- switch (data) {
221
- case "\x1b[A": // Up arrow
222
- case "k":
223
- this.selectedIndex = (this.selectedIndex - 1 + navItems.length) % navItems.length;
224
- break;
225
- case "\x1b[B": // Down arrow
226
- case "j":
227
- this.selectedIndex = (this.selectedIndex + 1) % navItems.length;
228
- break;
229
- case " ": // Space — toggle boolean settings
125
+ if (matchesKey(data, Key.up) || data === "k") {
126
+ this.selectedIndex = (this.selectedIndex - 1 + navItems.length) % navItems.length;
127
+ } else if (matchesKey(data, Key.down) || data === "j") {
128
+ this.selectedIndex = (this.selectedIndex + 1) % navItems.length;
129
+ } else if (matchesKey(data, Key.space)) {
130
+ this.toggleCurrentSetting();
131
+ } else if (matchesKey(data, Key.enter)) {
132
+ if (navItems[this.selectedIndex]?.type === "picker") {
133
+ this.enterPicker(navItems[this.selectedIndex] as PickerSetting);
134
+ } else {
230
135
  this.toggleCurrentSetting();
231
- break;
232
- case "\r": // Enter open picker or toggle
233
- if (navItems[this.selectedIndex]?.type === "picker") {
234
- this.enterPicker(navItems[this.selectedIndex] as PickerSetting);
235
- } else {
236
- this.toggleCurrentSetting();
237
- }
238
- break;
239
- case "\x1b": // Escape — save and close
240
- this.save();
241
- this.onClose?.();
242
- break;
136
+ }
137
+ } else if (matchesKey(data, Key.escape)) {
138
+ this.save();
139
+ this.onClose?.();
243
140
  }
244
141
  }
245
142
 
246
- /**
247
- * Handle input in picker mode.
248
- */
249
143
  private handlePickerInput(data: string, items: Array<{ id: string; label: string }>, pickerType: string): void {
250
- switch (data) {
251
- case "\x1b[A": // Up arrow
252
- case "k":
253
- this.scrollOffset = (this.scrollOffset - 1 + items.length) % items.length;
254
- break;
255
- case "\x1b[B": // Down arrow
256
- case "j":
257
- this.scrollOffset = (this.scrollOffset + 1) % items.length;
258
- break;
259
- case "\r": // Enter — select
260
- this.selectPickerItem(items[this.scrollOffset], pickerType);
261
- break;
262
- case "\x1b": // Escape — cancel
263
- this.mode = "settings";
264
- break;
144
+ if (matchesKey(data, Key.up) || data === "k") {
145
+ this.scrollOffset = (this.scrollOffset - 1 + items.length) % items.length;
146
+ } else if (matchesKey(data, Key.down) || data === "j") {
147
+ this.scrollOffset = (this.scrollOffset + 1) % items.length;
148
+ } else if (matchesKey(data, Key.enter)) {
149
+ this.selectPickerItem(items[this.scrollOffset], pickerType);
150
+ } else if (matchesKey(data, Key.escape)) {
151
+ this.mode = "settings";
265
152
  }
266
153
  }
267
154
 
268
- /**
269
- * Toggle the currently selected boolean setting.
270
- */
271
155
  private toggleCurrentSetting(): void {
272
156
  const navItems = SETTINGS.filter((s) => s.type !== "section");
273
157
  const item = navItems[this.selectedIndex];
274
158
  if (!item || item.type !== "boolean") return;
275
159
 
276
160
  const current = item.getValue(this.settings);
277
- if (item.section === "badge") {
278
- (this.settings.badge as any)[item.key] = !current;
279
- } else {
280
- (this.settings.diff as any)[item.key] = !current;
281
- }
161
+ (this.settings.badge as any)[item.key] = !current;
282
162
  this.save();
283
163
  }
284
164
 
285
- /**
286
- * Enter picker mode for a setting.
287
- */
288
165
  private enterPicker(item: PickerSetting): void {
289
166
  switch (item.pickerType) {
290
167
  case "model":
@@ -292,41 +169,19 @@ export class UtilSettingsTui implements Component {
292
169
  this.scrollOffset = this.getModelList().findIndex((m) => m.id === this.settings.badge.generationModel);
293
170
  if (this.scrollOffset < 0) this.scrollOffset = 0;
294
171
  break;
295
- case "theme":
296
- this.mode = "theme-picker";
297
- this.scrollOffset = getAllPresets().findIndex((p) => p.name === this.settings.diff.theme);
298
- if (this.scrollOffset < 0) this.scrollOffset = 0;
299
- break;
300
- case "shiki":
301
- this.mode = "shiki-picker";
302
- this.scrollOffset = SHIKI_THEMES.indexOf(this.settings.diff.shikiTheme);
303
- if (this.scrollOffset < 0) this.scrollOffset = 0;
304
- break;
305
172
  }
306
173
  }
307
174
 
308
- /**
309
- * Select an item in a picker.
310
- */
311
175
  private selectPickerItem(item: { id: string; label: string }, pickerType: string): void {
312
176
  switch (pickerType) {
313
177
  case "model":
314
178
  this.settings.badge.generationModel = item.id;
315
179
  break;
316
- case "theme":
317
- this.settings.diff.theme = item.id;
318
- break;
319
- case "shiki":
320
- this.settings.diff.shikiTheme = item.id;
321
- break;
322
180
  }
323
181
  this.mode = "settings";
324
182
  this.save();
325
183
  }
326
184
 
327
- /**
328
- * Get model list with "inherit" as first entry.
329
- */
330
185
  private getModelList(): Array<{ id: string; label: string }> {
331
186
  const list: Array<{ id: string; label: string }> = [
332
187
  { id: "inherit", label: "inherit (use parent model)" },
@@ -341,33 +196,10 @@ export class UtilSettingsTui implements Component {
341
196
  return list;
342
197
  }
343
198
 
344
- /**
345
- * Get diff theme preset list.
346
- */
347
- private getThemeList(): Array<{ id: string; label: string }> {
348
- return getAllPresets().map((p) => ({
349
- id: p.name,
350
- label: `${p.name} — ${p.description}`,
351
- }));
352
- }
353
-
354
- /**
355
- * Get Shiki theme list.
356
- */
357
- private getShikiList(): Array<{ id: string; label: string }> {
358
- return SHIKI_THEMES.map((t) => ({ id: t, label: t }));
359
- }
360
-
361
- /**
362
- * Save settings to disk.
363
- */
364
199
  private save(): void {
365
200
  writeUtilSettings(this.settings);
366
201
  }
367
202
 
368
- /**
369
- * Render the overlay.
370
- */
371
203
  render(width: number): string[] {
372
204
  const lines: string[] = [];
373
205
  const innerWidth = boxInnerWidth(width);
@@ -392,15 +224,12 @@ export class UtilSettingsTui implements Component {
392
224
  `${ansi.cyan}│${ansi.reset}`,
393
225
  );
394
226
 
395
- // Top border
396
227
  lines.push(`${ansi.cyan}╭${"─".repeat(innerWidth)}╮${ansi.reset}`);
397
228
 
398
- // Header
399
229
  add(`${ansi.bold}${ansi.cyan}⚙ Utility Settings${ansi.reset}`);
400
- add(`${ansi.dim}Configure badge and diff rendering${ansi.reset}`);
230
+ add(`${ansi.dim}Configure badge${ansi.reset}`);
401
231
  addEmpty();
402
232
 
403
- // Settings list
404
233
  const navItems = SETTINGS.filter((s) => s.type !== "section");
405
234
  let navIndex = 0;
406
235
 
@@ -422,9 +251,8 @@ export class UtilSettingsTui implements Component {
422
251
  add(` ${ansi.gray}${item.description}${ansi.reset}`);
423
252
  } else if (item.type === "picker") {
424
253
  const value = item.getValue(this.settings);
425
- const icon = item.pickerType === "model" ? "⚙" : "🎨";
426
254
  add(
427
- `${selector} ${ansi.yellow}${icon}${ansi.reset} ${labelColor}${item.label}${ansi.reset}: ${ansi.white}${value}${ansi.reset}`,
255
+ `${selector} ${ansi.yellow}⚙${ansi.reset} ${labelColor}${item.label}${ansi.reset}: ${ansi.white}${value}${ansi.reset}`,
428
256
  );
429
257
  add(` ${ansi.gray}${item.description}${ansi.reset}`);
430
258
  if (isSelected) {
@@ -435,7 +263,6 @@ export class UtilSettingsTui implements Component {
435
263
  navIndex++;
436
264
  }
437
265
 
438
- // Picker overlay (inline)
439
266
  if (this.mode !== "settings") {
440
267
  addEmpty();
441
268
  let items: Array<{ id: string; label: string }> = [];
@@ -446,14 +273,6 @@ export class UtilSettingsTui implements Component {
446
273
  items = this.getModelList();
447
274
  title = "Available Models";
448
275
  break;
449
- case "theme-picker":
450
- items = this.getThemeList();
451
- title = "Diff Theme Presets";
452
- break;
453
- case "shiki-picker":
454
- items = this.getShikiList();
455
- title = "Shiki Themes";
456
- break;
457
276
  }
458
277
 
459
278
  add(`${ansi.bold}${ansi.cyan}── ${title} ──${ansi.reset}`);
@@ -480,7 +299,6 @@ export class UtilSettingsTui implements Component {
480
299
  }
481
300
  }
482
301
 
483
- // Footer
484
302
  addEmpty();
485
303
 
486
304
  if (this.mode === "settings") {
@@ -490,7 +308,6 @@ export class UtilSettingsTui implements Component {
490
308
  add(`${ansi.dim}↑↓ navigate • Enter select • Esc cancel${ansi.reset}`);
491
309
  }
492
310
 
493
- // Bottom border
494
311
  lines.push(`${ansi.cyan}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
495
312
 
496
313
  return lines;