@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.
- package/package.json +8 -6
- package/src/commands.ts +4 -5
- package/src/herdr-sync.ts +136 -0
- package/src/index.ts +11 -68
- package/src/lifecycle/cleanup.ts +2 -16
- package/src/{diff/settings.ts → settings.ts} +42 -80
- package/src/tools/env.ts +0 -0
- package/src/tui/name-badge-state.ts +18 -2
- package/src/tui/name-badge.ts +5 -4
- package/src/tui/util-settings-tui.ts +41 -224
- package/src/cache/ttl-cache.ts +0 -311
- package/src/diff/highlighter.ts +0 -352
- package/src/diff/parser.ts +0 -191
- package/src/diff/renderer.ts +0 -395
- package/src/diff/theme.ts +0 -316
- package/src/diff/wrapper.ts +0 -339
- package/src/display/capabilities.ts +0 -214
- package/src/display/width.ts +0 -226
- package/src/tools/batch.ts +0 -229
- package/src/tui/badge-settings-tui.ts +0 -388
- package/src/tui/badge-settings.ts +0 -67
- package/src/tui/settings-inspector.ts +0 -303
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/utility",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Utility commands and tools for Pi coding agent — lifecycle, diagnostics, cache, analytics, display, batch execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -36,9 +36,7 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@pi-unipi/core": "2.6.1"
|
|
40
|
-
"diff": "^7.0.0",
|
|
41
|
-
"shiki": "^4.0.2"
|
|
39
|
+
"@pi-unipi/core": "2.6.1"
|
|
42
40
|
},
|
|
43
41
|
"devDependencies": {
|
|
44
42
|
"@types/diff": "^7.0.2"
|
|
@@ -52,8 +50,12 @@
|
|
|
52
50
|
"test": "npx tsx --test tests/**/*.test.ts"
|
|
53
51
|
},
|
|
54
52
|
"pi": {
|
|
55
|
-
"extensions": [
|
|
56
|
-
|
|
53
|
+
"extensions": [
|
|
54
|
+
"./src/index.ts"
|
|
55
|
+
],
|
|
56
|
+
"skills": [
|
|
57
|
+
"./skills"
|
|
58
|
+
],
|
|
57
59
|
"prompts": [],
|
|
58
60
|
"themes": []
|
|
59
61
|
}
|
package/src/commands.ts
CHANGED
|
@@ -21,8 +21,7 @@ import { cleanupStale, formatCleanupReport } from "./lifecycle/cleanup.js";
|
|
|
21
21
|
import { runDiagnostics, formatDiagnosticsReport } from "./diagnostics/engine.js";
|
|
22
22
|
import { getEnvironmentInfo, formatEnvironmentInfo } from "./tools/env.js";
|
|
23
23
|
import type { NameBadgeState } from "./tui/name-badge-state.js";
|
|
24
|
-
import { readBadgeSettings, updateBadgeSetting, formatBadgeSettings } from "./
|
|
25
|
-
import { BadgeSettingsTui } from "./tui/badge-settings-tui.js";
|
|
24
|
+
import { readBadgeSettings, updateBadgeSetting, formatBadgeSettings } from "./settings.js";
|
|
26
25
|
import { UtilSettingsTui } from "./tui/util-settings-tui.js";
|
|
27
26
|
|
|
28
27
|
/** Send a markdown response via pi.sendMessage */
|
|
@@ -77,14 +76,14 @@ export function registerNameBadgeCommands(
|
|
|
77
76
|
|
|
78
77
|
// ─── /unipi:badge-toggle — configure badge settings ─────────────────────
|
|
79
78
|
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.BADGE_TOGGLE}`, {
|
|
80
|
-
description: "Configure badge settings (autoGen, badgeEnabled, agentTool)",
|
|
79
|
+
description: "Configure badge settings (autoGen, badgeEnabled, agentTool, herdrSync)",
|
|
81
80
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
82
81
|
// Parse args: /unipi:badge-settings [key] [on|off]
|
|
83
82
|
const parts = args.trim().split(/\s+/);
|
|
84
83
|
if (parts.length >= 2 && parts[0]) {
|
|
85
|
-
const key = parts[0] as "autoGen" | "badgeEnabled" | "agentTool";
|
|
84
|
+
const key = parts[0] as "autoGen" | "badgeEnabled" | "agentTool" | "herdrSync";
|
|
86
85
|
const value = parts[1]?.toLowerCase();
|
|
87
|
-
if ("autoGen|badgeEnabled|agentTool".includes(key)) {
|
|
86
|
+
if ("autoGen|badgeEnabled|agentTool|herdrSync".includes(key)) {
|
|
88
87
|
const boolValue = value === "on" || value === "true" || value === "1";
|
|
89
88
|
updateBadgeSetting(key, boolValue);
|
|
90
89
|
ctx.ui.notify(`Badge ${key} set to ${boolValue}`, "info");
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Herdr pane title sync
|
|
3
|
+
*
|
|
4
|
+
* When running inside a Herdr pane, pushes the pi session name to Herdr's
|
|
5
|
+
* socket API (`pane.report_metadata` with `title`). Herdr displays the title
|
|
6
|
+
* in the pane border / agent panel — always visible, never scrolls away.
|
|
7
|
+
*
|
|
8
|
+
* This is the scroll-proof session name display: the in-TUI badge overlay
|
|
9
|
+
* scrolls with content (pi renders to the primary buffer), but Herdr's pane
|
|
10
|
+
* border is drawn by Herdr itself outside the terminal scrollback.
|
|
11
|
+
*
|
|
12
|
+
* Detection: Herdr sets HERDR_ENV=1 and HERDR_SOCKET_PATH + HERDR_PANE_ID
|
|
13
|
+
* in every pane it spawns (same env the official herdr-pi integration uses).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createConnection } from "node:net";
|
|
17
|
+
|
|
18
|
+
/** Push timeout — never block the agent on a slow socket. */
|
|
19
|
+
const SEND_TIMEOUT_MS = 500;
|
|
20
|
+
|
|
21
|
+
export interface HerdrEnv {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
socketPath?: string;
|
|
24
|
+
paneId?: string;
|
|
25
|
+
tabId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read Herdr environment (call once per session_start, cheap). */
|
|
29
|
+
export function detectHerdr(): HerdrEnv {
|
|
30
|
+
const env = process.env.HERDR_ENV;
|
|
31
|
+
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
32
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
33
|
+
const tabId = process.env.HERDR_TAB_ID;
|
|
34
|
+
if (env === "1" && socketPath && paneId) {
|
|
35
|
+
return { enabled: true, socketPath, paneId, tabId };
|
|
36
|
+
}
|
|
37
|
+
return { enabled: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sendRequest(socketPath: string, request: unknown): Promise<void> {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let done = false;
|
|
43
|
+
const finish = () => {
|
|
44
|
+
if (done) return;
|
|
45
|
+
done = true;
|
|
46
|
+
socket.destroy();
|
|
47
|
+
resolve();
|
|
48
|
+
};
|
|
49
|
+
const socket = createConnection(socketPath);
|
|
50
|
+
socket.on("error", finish);
|
|
51
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
52
|
+
socket.on("data", finish);
|
|
53
|
+
socket.on("end", finish);
|
|
54
|
+
const timeout = setTimeout(finish, SEND_TIMEOUT_MS);
|
|
55
|
+
timeout.unref?.();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let reportSeq = Date.now();
|
|
60
|
+
|
|
61
|
+
/** Only rename the tab when its label is a default numeric one (1-3 digits) — never clobber a user-set name. */
|
|
62
|
+
const DEFAULT_TAB_LABEL = /^\d{1,3}$/;
|
|
63
|
+
|
|
64
|
+
async function fetchJson(socketPath: string, method: string, params: Record<string, unknown>, extract: (result: any) => Record<string, unknown> | null): Promise<Record<string, unknown> | null> {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
let done = false;
|
|
67
|
+
const finish = (value: Record<string, unknown> | null) => {
|
|
68
|
+
if (done) return;
|
|
69
|
+
done = true;
|
|
70
|
+
socket.destroy();
|
|
71
|
+
resolve(value);
|
|
72
|
+
};
|
|
73
|
+
const socket = createConnection(socketPath);
|
|
74
|
+
let buffer = "";
|
|
75
|
+
socket.on("error", () => finish(null));
|
|
76
|
+
socket.on("connect", () =>
|
|
77
|
+
socket.write(
|
|
78
|
+
`${JSON.stringify({ id: `unipi:fetch:${method}:${Date.now()}`, method, params })}\n`,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
socket.on("data", (chunk) => {
|
|
82
|
+
buffer += chunk.toString();
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(buffer);
|
|
85
|
+
finish(extract(parsed?.result));
|
|
86
|
+
} catch {
|
|
87
|
+
/* keep buffering */
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
socket.on("end", () => finish(null));
|
|
91
|
+
const timeout = setTimeout(() => finish(null), SEND_TIMEOUT_MS);
|
|
92
|
+
timeout.unref?.();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Push the session name to Herdr as the pane title, and as the tab label
|
|
98
|
+
* when the tab still has its default numeric label.
|
|
99
|
+
* Fire-and-forget: never throws, resolves after send timeout at worst.
|
|
100
|
+
*/
|
|
101
|
+
export async function syncPaneTitle(env: HerdrEnv, sessionName: string | null): Promise<void> {
|
|
102
|
+
if (!env.enabled || !env.socketPath || !env.paneId) return;
|
|
103
|
+
reportSeq += 1;
|
|
104
|
+
const params: Record<string, unknown> = {
|
|
105
|
+
pane_id: env.paneId,
|
|
106
|
+
source: "unipi:badge",
|
|
107
|
+
seq: reportSeq,
|
|
108
|
+
};
|
|
109
|
+
if (sessionName) {
|
|
110
|
+
params.title = sessionName;
|
|
111
|
+
} else {
|
|
112
|
+
params.clear_title = true;
|
|
113
|
+
}
|
|
114
|
+
// Best effort — errors are swallowed by sendRequest's error handler.
|
|
115
|
+
await sendRequest(env.socketPath, {
|
|
116
|
+
id: `unipi:badge:title:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
117
|
+
method: "pane.report_metadata",
|
|
118
|
+
params,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Also sync the tab label so the name is visible in the tab bar (single-pane
|
|
122
|
+
// tabs don't render pane borders, so the pane title alone is invisible there).
|
|
123
|
+
// Only rename when the label is still the default numeric one — a user-set
|
|
124
|
+
// name always wins.
|
|
125
|
+
if (sessionName && env.tabId) {
|
|
126
|
+
const tab = await fetchJson(env.socketPath, "tab.get", { tab_id: env.tabId }, (r) => r?.tab ?? null);
|
|
127
|
+
const label = tab?.label;
|
|
128
|
+
if (typeof label !== "string" || DEFAULT_TAB_LABEL.test(label)) {
|
|
129
|
+
await sendRequest(env.socketPath, {
|
|
130
|
+
id: `unipi:badge:tab:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
131
|
+
method: "tab.rename",
|
|
132
|
+
params: { tab_id: env.tabId, label: sessionName },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -26,16 +26,14 @@ import {
|
|
|
26
26
|
} from "@pi-unipi/core";
|
|
27
27
|
import { registerUtilityCommands, registerNameBadgeCommands } from "./commands.js";
|
|
28
28
|
import { NameBadgeState } from "./tui/name-badge-state.js";
|
|
29
|
-
import { readBadgeSettings } from "./
|
|
30
|
-
import { readDiffSettings } from "./diff/settings.js";
|
|
31
|
-
import { registerEnhancedWriteTool, registerEnhancedEditTool } from "./diff/wrapper.js";
|
|
29
|
+
import { readBadgeSettings } from "./settings.js";
|
|
32
30
|
import { getLifecycle } from "./lifecycle/process.js";
|
|
33
31
|
import { getAnalyticsCollector } from "./analytics/collector.js";
|
|
34
32
|
import { registerInfoScreen } from "./info-screen.js";
|
|
35
33
|
import { PrefixCacheTracker, formatPrefixCacheStats } from "./prefix-cache.js";
|
|
36
34
|
|
|
37
35
|
/** Re-export readBadgeSettings for cross-package use */
|
|
38
|
-
export { readBadgeSettings } from "./
|
|
36
|
+
export { readBadgeSettings } from "./settings.js";
|
|
39
37
|
|
|
40
38
|
/** Package version */
|
|
41
39
|
const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -155,12 +153,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
155
153
|
// Restore name badge if it was visible in previous session
|
|
156
154
|
await nameBadgeState.restore(pi, ctx);
|
|
157
155
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
156
|
+
// Auto-show badge on session start if enabled in settings and UI is available.
|
|
157
|
+
// The badge shows "Set a name" placeholder until a name is generated.
|
|
158
|
+
// Previously the badge only showed after the first agent_end, which meant it
|
|
159
|
+
// was absent on restart until the user sent a message.
|
|
160
|
+
if (ctx?.hasUI && !nameBadgeState.isVisible()) {
|
|
161
|
+
const badgeSettings = readBadgeSettings();
|
|
162
|
+
if (badgeSettings.badgeEnabled) {
|
|
163
|
+
await nameBadgeState.show(pi, ctx);
|
|
164
|
+
}
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
// Write model cache for TUI components
|
|
@@ -274,64 +275,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
274
275
|
* Register utility tools.
|
|
275
276
|
*/
|
|
276
277
|
function registerUtilityTools(pi: ExtensionAPI, nameBadgeState: NameBadgeState): void {
|
|
277
|
-
// ctx_batch — atomic batch execution
|
|
278
|
-
pi.registerTool({
|
|
279
|
-
name: UTILITY_TOOLS.BATCH,
|
|
280
|
-
label: "Batch Execute",
|
|
281
|
-
description:
|
|
282
|
-
"Execute a batch of commands atomically with rollback support. " +
|
|
283
|
-
"Accepts an array of {type, name, args} objects. " +
|
|
284
|
-
"Options: failFast (default true), commandTimeoutMs, totalTimeoutMs.",
|
|
285
|
-
promptSnippet: "Run multiple commands as an atomic batch.",
|
|
286
|
-
parameters: {
|
|
287
|
-
type: "object",
|
|
288
|
-
properties: {
|
|
289
|
-
commands: {
|
|
290
|
-
type: "array",
|
|
291
|
-
items: {
|
|
292
|
-
type: "object",
|
|
293
|
-
properties: {
|
|
294
|
-
type: { type: "string", enum: ["command", "tool", "search"] },
|
|
295
|
-
name: { type: "string" },
|
|
296
|
-
args: { type: "object" },
|
|
297
|
-
},
|
|
298
|
-
required: ["type", "name"],
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
options: {
|
|
302
|
-
type: "object",
|
|
303
|
-
properties: {
|
|
304
|
-
failFast: { type: "boolean" },
|
|
305
|
-
commandTimeoutMs: { type: "number" },
|
|
306
|
-
totalTimeoutMs: { type: "number" },
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
},
|
|
310
|
-
required: ["commands"],
|
|
311
|
-
},
|
|
312
|
-
async execute(_toolCallId, params) {
|
|
313
|
-
const { commands, options } = params as unknown as {
|
|
314
|
-
commands: Array<{ type: string; name: string; args?: Record<string, unknown> }>;
|
|
315
|
-
options?: Record<string, unknown>;
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
// Tool implementation delegates to batch executor
|
|
319
|
-
// The actual executor must be provided by the host
|
|
320
|
-
return {
|
|
321
|
-
content: [
|
|
322
|
-
{
|
|
323
|
-
type: "text",
|
|
324
|
-
text:
|
|
325
|
-
"ctx_batch requires a command executor from the host environment. " +
|
|
326
|
-
`Received ${commands.length} commands. ` +
|
|
327
|
-
"Use BatchBuilder or executeBatch() directly in code.",
|
|
328
|
-
},
|
|
329
|
-
],
|
|
330
|
-
details: { commands, options },
|
|
331
|
-
};
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
|
|
335
278
|
// ctx_env — environment info
|
|
336
279
|
pi.registerTool({
|
|
337
280
|
name: UTILITY_TOOLS.ENV,
|
package/src/lifecycle/cleanup.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Cleans stale DBs, temp files, old sessions across all unipi modules.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync, statSync, readdirSync, unlinkSync,
|
|
7
|
+
import { existsSync, statSync, readdirSync, unlinkSync, rmSync } from "node:fs";
|
|
8
8
|
import { join, resolve, basename } from "node:path";
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import type { CleanupReport, CleanupResult, CleanupOptions } from "../types.js";
|
|
@@ -229,21 +229,7 @@ function cleanSessions(options: Required<CleanupOptions>): CleanupResult {
|
|
|
229
229
|
result.paths.push(fullPath);
|
|
230
230
|
if (!options.dryRun) {
|
|
231
231
|
try {
|
|
232
|
-
|
|
233
|
-
const removeRecursive = (dir: string) => {
|
|
234
|
-
const items = readdirSync(dir);
|
|
235
|
-
for (const item of items) {
|
|
236
|
-
const itemPath = join(dir, item);
|
|
237
|
-
const itemStats = statSync(itemPath);
|
|
238
|
-
if (itemStats.isDirectory()) {
|
|
239
|
-
removeRecursive(itemPath);
|
|
240
|
-
} else {
|
|
241
|
-
unlinkSync(itemPath);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
rmdirSync(dir);
|
|
245
|
-
};
|
|
246
|
-
removeRecursive(fullPath);
|
|
232
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
247
233
|
result.removed++;
|
|
248
234
|
} catch {
|
|
249
235
|
// Best effort
|
|
@@ -1,68 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @pi-unipi/utility —
|
|
2
|
+
* @pi-unipi/utility — Settings Manager
|
|
3
3
|
*
|
|
4
|
-
* Manages
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 }
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
130
|
+
settings.badge = { ...settings.badge, ...partial };
|
|
182
131
|
writeUtilSettings(settings);
|
|
183
132
|
}
|
|
184
133
|
|
|
185
|
-
/**
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 "
|
|
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
|
}
|
package/src/tui/name-badge.ts
CHANGED
|
@@ -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
|
|
100
|
-
|
|
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 [
|
|
107
|
+
return [contentLine];
|
|
107
108
|
}
|
|
108
109
|
}
|