@pi-unipi/utility 2.6.1 → 2.9.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.
@@ -1,68 +1,46 @@
1
1
  /**
2
- * @pi-unipi/utility — Unified Settings Manager
2
+ * @pi-unipi/utility — Settings Manager
3
3
  *
4
- * Manages both badge and diff settings in a single `.unipi/config/util-settings.json` file.
4
+ * Manages badge settings in `.unipi/config/util-settings.json`.
5
5
  * Migrates from legacy `badge.json` on first read.
6
6
  */
7
7
 
8
8
  import * as fs from "node:fs";
9
9
  import * as path from "node:path";
10
10
 
11
- /** Diff rendering settings */
12
- export interface DiffSettings {
13
- /** Enable Shiki-powered diff rendering for write/edit tools */
14
- enabled: boolean;
15
- /** Diff theme preset: "default" | "midnight" | "subtle" | "neon" */
16
- theme: string;
17
- /** Shiki syntax theme name */
18
- shikiTheme: string;
19
- /** Minimum terminal columns for split view */
20
- splitMinWidth: number;
21
- }
22
-
23
- /** Badge settings (matches existing BadgeSettings interface) */
11
+ /** Badge settings */
24
12
  export interface BadgeSettingsSection {
25
13
  autoGen: boolean;
26
14
  badgeEnabled: boolean;
27
15
  agentTool: boolean;
28
16
  generationModel: string;
17
+ /** Sync session name to herdr pane title + tab label (when running inside herdr). */
18
+ herdrSync: boolean;
29
19
  }
30
20
 
31
21
  /** Unified utility settings */
32
22
  export interface UtilSettings {
33
23
  badge: BadgeSettingsSection;
34
- diff: DiffSettings;
35
24
  }
36
25
 
37
- /** Default diff settings */
38
- const DEFAULT_DIFF_SETTINGS: DiffSettings = {
39
- enabled: true,
40
- theme: "default",
41
- shikiTheme: "github-dark",
42
- splitMinWidth: 150,
43
- };
44
-
45
26
  /** Default badge settings */
46
27
  const DEFAULT_BADGE_SETTINGS: BadgeSettingsSection = {
47
28
  autoGen: true,
48
29
  badgeEnabled: true,
49
30
  agentTool: true,
50
31
  generationModel: "inherit",
32
+ herdrSync: true,
51
33
  };
52
34
 
53
35
  /** Default unified settings */
54
36
  const DEFAULT_SETTINGS: UtilSettings = {
55
37
  badge: { ...DEFAULT_BADGE_SETTINGS },
56
- diff: { ...DEFAULT_DIFF_SETTINGS },
57
38
  };
58
39
 
59
40
  /** Config file paths */
60
41
  const UTIL_SETTINGS_FILE = ".unipi/config/util-settings.json";
61
42
  const BADGE_CONFIG_FILE = ".unipi/config/badge.json";
62
43
 
63
- /**
64
- * Get absolute path for a config file relative to cwd.
65
- */
66
44
  function getConfigPath(file: string): string {
67
45
  return path.resolve(process.cwd(), file);
68
46
  }
@@ -81,58 +59,41 @@ function readLegacyBadgeSettings(): BadgeSettingsSection | null {
81
59
  badgeEnabled: typeof parsed.badgeEnabled === "boolean" ? parsed.badgeEnabled : DEFAULT_BADGE_SETTINGS.badgeEnabled,
82
60
  agentTool: typeof parsed.agentTool === "boolean" ? parsed.agentTool : DEFAULT_BADGE_SETTINGS.agentTool,
83
61
  generationModel: typeof parsed.generationModel === "string" ? parsed.generationModel : DEFAULT_BADGE_SETTINGS.generationModel,
62
+ herdrSync: typeof parsed.herdrSync === "boolean" ? parsed.herdrSync : DEFAULT_BADGE_SETTINGS.herdrSync,
84
63
  };
85
64
  } catch {
86
65
  return null;
87
66
  }
88
67
  }
89
68
 
90
- /**
91
- * Atomic write: write to temp file then rename.
92
- * Prevents corruption if two instances write simultaneously.
93
- */
94
69
  function atomicWrite(filePath: string, data: string): void {
95
70
  const tmpPath = filePath + ".tmp";
96
71
  fs.writeFileSync(tmpPath, data, "utf-8");
97
72
  fs.renameSync(tmpPath, filePath);
98
73
  }
99
74
 
100
- /**
101
- * Read the unified util-settings.json.
102
- * On first read, migrates from badge.json if it exists.
103
- * Returns defaults if no config exists.
104
- */
105
75
  export function readUtilSettings(): UtilSettings {
106
76
  try {
107
77
  const configPath = getConfigPath(UTIL_SETTINGS_FILE);
108
78
 
109
- // Check if unified config exists
110
79
  if (fs.existsSync(configPath)) {
111
80
  const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
112
81
  return normalizeSettings(parsed);
113
82
  }
114
83
 
115
- // Migration: import from badge.json if it exists
116
84
  const legacyBadge = readLegacyBadgeSettings();
117
85
  if (legacyBadge) {
118
- const migrated: UtilSettings = {
119
- badge: legacyBadge,
120
- diff: { ...DEFAULT_DIFF_SETTINGS },
121
- };
86
+ const migrated: UtilSettings = { badge: legacyBadge };
122
87
  writeUtilSettings(migrated);
123
88
  return migrated;
124
89
  }
125
90
 
126
- // No config at all return defaults (don't write yet)
127
- return { ...DEFAULT_SETTINGS, badge: { ...DEFAULT_BADGE_SETTINGS }, diff: { ...DEFAULT_DIFF_SETTINGS } };
91
+ return { ...DEFAULT_SETTINGS, badge: { ...DEFAULT_BADGE_SETTINGS } };
128
92
  } catch {
129
- return { ...DEFAULT_SETTINGS, badge: { ...DEFAULT_BADGE_SETTINGS }, diff: { ...DEFAULT_DIFF_SETTINGS } };
93
+ return { ...DEFAULT_SETTINGS, badge: { ...DEFAULT_BADGE_SETTINGS } };
130
94
  }
131
95
  }
132
96
 
133
- /**
134
- * Write the full unified settings to disk.
135
- */
136
97
  export function writeUtilSettings(settings: UtilSettings): void {
137
98
  try {
138
99
  const configPath = getConfigPath(UTIL_SETTINGS_FILE);
@@ -146,9 +107,6 @@ export function writeUtilSettings(settings: UtilSettings): void {
146
107
  }
147
108
  }
148
109
 
149
- /**
150
- * Normalize a parsed JSON object into valid UtilSettings.
151
- */
152
110
  function normalizeSettings(parsed: any): UtilSettings {
153
111
  return {
154
112
  badge: {
@@ -156,44 +114,48 @@ function normalizeSettings(parsed: any): UtilSettings {
156
114
  badgeEnabled: typeof parsed?.badge?.badgeEnabled === "boolean" ? parsed.badge.badgeEnabled : DEFAULT_BADGE_SETTINGS.badgeEnabled,
157
115
  agentTool: typeof parsed?.badge?.agentTool === "boolean" ? parsed.badge.agentTool : DEFAULT_BADGE_SETTINGS.agentTool,
158
116
  generationModel: typeof parsed?.badge?.generationModel === "string" ? parsed.badge.generationModel : DEFAULT_BADGE_SETTINGS.generationModel,
159
- },
160
- diff: {
161
- enabled: typeof parsed?.diff?.enabled === "boolean" ? parsed.diff.enabled : DEFAULT_DIFF_SETTINGS.enabled,
162
- theme: typeof parsed?.diff?.theme === "string" ? parsed.diff.theme : DEFAULT_DIFF_SETTINGS.theme,
163
- shikiTheme: typeof parsed?.diff?.shikiTheme === "string" ? parsed.diff.shikiTheme : DEFAULT_DIFF_SETTINGS.shikiTheme,
164
- splitMinWidth: typeof parsed?.diff?.splitMinWidth === "number" ? parsed.diff.splitMinWidth : DEFAULT_DIFF_SETTINGS.splitMinWidth,
117
+ herdrSync: typeof parsed?.badge?.herdrSync === "boolean" ? parsed.badge.herdrSync : DEFAULT_BADGE_SETTINGS.herdrSync,
165
118
  },
166
119
  };
167
120
  }
168
121
 
169
- /**
170
- * Read only the diff settings section.
171
- */
172
- export function readDiffSettings(): DiffSettings {
173
- return readUtilSettings().diff;
122
+ /** Read only the badge settings section. */
123
+ export function readBadgeSettings(): BadgeSettingsSection {
124
+ return readUtilSettings().badge;
174
125
  }
175
126
 
176
- /**
177
- * Write partial diff settings (merged with existing).
178
- */
179
- export function writeDiffSettings(partial: Partial<DiffSettings>): void {
127
+ /** Write partial badge settings (merged with existing). */
128
+ export function writeBadgeSettings(partial: Partial<BadgeSettingsSection>): void {
180
129
  const settings = readUtilSettings();
181
- settings.diff = { ...settings.diff, ...partial };
130
+ settings.badge = { ...settings.badge, ...partial };
182
131
  writeUtilSettings(settings);
183
132
  }
184
133
 
185
- /**
186
- * Read only the badge settings section.
187
- */
188
- export function readBadgeSettingsFromUtil(): BadgeSettingsSection {
189
- return readUtilSettings().badge;
134
+ /** Update a single badge setting. */
135
+ export function updateBadgeSetting<K extends keyof BadgeSettingsSection>(
136
+ key: K,
137
+ value: BadgeSettingsSection[K],
138
+ ): BadgeSettingsSection {
139
+ const settings = readBadgeSettings();
140
+ settings[key] = value;
141
+ writeBadgeSettings(settings);
142
+ return settings;
190
143
  }
191
144
 
192
- /**
193
- * Write partial badge settings (merged with existing).
194
- */
195
- export function writeBadgeSettingsToUtil(partial: Partial<BadgeSettingsSection>): void {
196
- const settings = readUtilSettings();
197
- settings.badge = { ...settings.badge, ...partial };
198
- writeUtilSettings(settings);
145
+ /** Format badge settings for display. */
146
+ export function formatBadgeSettings(settings: BadgeSettingsSection): string {
147
+ const toggle = (v: boolean) => (v ? "✓ enabled" : "✗ disabled");
148
+ return [
149
+ "## Badge Settings",
150
+ "",
151
+ `| Setting | Status | Description |`,
152
+ `|---------|--------|-------------|`,
153
+ `| Auto Generate | ${toggle(settings.autoGen)} | Generate name on first message |`,
154
+ `| Badge Enabled | ${toggle(settings.badgeEnabled)} | Show badge overlay |`,
155
+ `| Agent Tool | ${toggle(settings.agentTool)} | Allow agents to call set_session_name |`,
156
+ `| Herdr Sync | ${toggle(settings.herdrSync)} | Sync session name to herdr tab/pane title |`,
157
+ `| Generation Model | ${settings.generationModel} | Model for badge name generation |`,
158
+ "",
159
+ `Config: .unipi/config/util-settings.json`,
160
+ ].join("\n");
199
161
  }
package/src/tools/env.ts CHANGED
File without changes
@@ -11,7 +11,8 @@
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { UNIPI_EVENTS, emitEvent } from "@pi-unipi/core";
13
13
  import { NameBadgeComponent } from "./name-badge.js";
14
- import { readBadgeSettings } from "./badge-settings.js";
14
+ import { readBadgeSettings } from "../settings.js";
15
+ import { detectHerdr, syncPaneTitle, type HerdrEnv } from "../herdr-sync.js";
15
16
 
16
17
  /** Overlay handle from ctx.ui.custom() */
17
18
  interface OverlayHandle {
@@ -54,6 +55,7 @@ export class NameBadgeState {
54
55
  private pollTimer: ReturnType<typeof setInterval> | null = null;
55
56
  private component: NameBadgeComponent | null = null;
56
57
  private genTimeout: ReturnType<typeof setTimeout> | null = null;
58
+ private herdr: HerdrEnv = { enabled: false };
57
59
 
58
60
  /** Whether the badge is currently visible */
59
61
  isVisible(): boolean {
@@ -89,6 +91,13 @@ export class NameBadgeState {
89
91
  ): Promise<void> {
90
92
  if (this.overlayHandle) return; // Already showing
91
93
 
94
+ // Detect Herdr once per session; sync the current name to its pane title.
95
+ this.herdr = detectHerdr();
96
+ if (this.herdr.enabled && readBadgeSettings().herdrSync) {
97
+ const name = this.safeGetName(pi);
98
+ void syncPaneTitle(this.herdr, name);
99
+ }
100
+
92
101
  const name = this.safeGetName(pi);
93
102
  this.currentName = name;
94
103
  this.visible = true;
@@ -169,7 +178,6 @@ export class NameBadgeState {
169
178
  const badgeEntry = entries.findLast(
170
179
  (e: any) => e.type === "custom" && e.customType === BADGE_ENTRY_TYPE,
171
180
  );
172
-
173
181
  if (badgeEntry?.data?.visible) {
174
182
  await this.show(pi, ctx);
175
183
  }
@@ -224,6 +232,10 @@ export class NameBadgeState {
224
232
  this.currentName = name;
225
233
  this.component?.setName(name);
226
234
  this.overlayHandle?.requestRender?.();
235
+ // Sync to Herdr pane title (scroll-proof display)
236
+ if (readBadgeSettings().herdrSync) {
237
+ void syncPaneTitle(this.herdr, name);
238
+ }
227
239
  // Clear generation timeout if active
228
240
  this.clearGenTimeout();
229
241
  } catch {
@@ -251,6 +263,10 @@ export class NameBadgeState {
251
263
  this.currentName = name;
252
264
  this.component?.setName(name);
253
265
  this.overlayHandle?.requestRender?.();
266
+ // Sync to Herdr pane title (scroll-proof display)
267
+ if (readBadgeSettings().herdrSync) {
268
+ void syncPaneTitle(this.herdr, name);
269
+ }
254
270
  }
255
271
  }, POLL_INTERVAL_MS);
256
272
  }
@@ -96,13 +96,14 @@ export class NameBadgeComponent implements Component {
96
96
  ? this.theme.fg(fgColor as any, displayText)
97
97
  : displayText;
98
98
 
99
- // Build lines with opaque background spanning full width
100
- const topLine = bgFn(border("╭" + "─".repeat(innerWidth) + "╮"));
99
+ // Build single line with opaque background (no top/bottom borders to
100
+ // minimize vertical blocking the badge stays at the top of the screen
101
+ // and a single line blocks far less content than a 3-line box when
102
+ // scrolling up to read history).
101
103
  const contentLine = bgFn(
102
104
  border("│") + " ".repeat(leftPad) + nameStyled + " ".repeat(rightPad) + border("│"),
103
105
  );
104
- const bottomLine = bgFn(border("╰" + "─".repeat(innerWidth) + "╯"));
105
106
 
106
- return [topLine, contentLine, bottomLine];
107
+ return [contentLine];
107
108
  }
108
109
  }