Skip to content

Commit d5f4ffd

Browse files
pi-extension: make startup notification opt-out via quietStartup (#308)
The status bar already shows the active ponytail mode at all times, but the startup toast is also a deliberate trust-heartbeat for a behavior-modifying extension (it confirms ponytail loaded, in which mode, and survives session resume where the persisted mode may differ from the default). Default behavior is preserved. Users who prefer a clean startup can now silence it via: - config: ~/.config/ponytail/config.json -> { "quietStartup": true } - env var: PONYTAIL_QUIET_STARTUP=1 Resolution order mirrors the existing defaultMode setting: env var > config file > default (false). Added readQuietStartup export and a test covering all three layers. Co-authored-by: blissini <blissini@users.noreply.github.com> Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
1 parent 9888cdf commit d5f4ffd

3 files changed

Lines changed: 57 additions & 2 deletions

File tree

‎hooks/ponytail-config.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,23 @@ function getDefaultMode() {
9696
return DEFAULT_MODE;
9797
}
9898

99+
// Silence the pi "Ponytail loaded" startup toast while keeping ponytail active.
100+
// PONYTAIL_QUIET_STARTUP=1 (or any truthy value; 0/false/empty mean "show it")
101+
// takes precedence, else config.quietStartup === true. Mirrors getHideStatus.
102+
function getQuietStartup() {
103+
const env = process.env.PONYTAIL_QUIET_STARTUP;
104+
if (env !== undefined) {
105+
const v = env.trim().toLowerCase();
106+
return v !== '' && v !== '0' && v !== 'false' && v !== 'no';
107+
}
108+
try {
109+
const config = JSON.parse(fs.readFileSync(getConfigPath(), 'utf8').replace(/^\uFEFF/, ''));
110+
return config.quietStartup === true;
111+
} catch (_) {
112+
return false;
113+
}
114+
}
115+
99116
// Hide the status-bar indicator while keeping ponytail active (#324).
100117
// PONYTAIL_HIDE_STATUS=1 (or any truthy value; 0/false/empty mean "don't hide")
101118
// takes precedence, else config.hideStatus === true.
@@ -138,6 +155,7 @@ module.exports = {
138155
getConfigPath,
139156
getClaudeDir,
140157
getHideStatus,
158+
getQuietStartup,
141159
isShellSafe,
142160
normalizeMode,
143161
normalizeConfigMode,

‎pi-extension/index.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
DEFAULT_MODE,
66
RUNTIME_MODES,
77
getDefaultMode,
8+
getQuietStartup,
89
getHideStatus,
910
normalizeMode,
1011
normalizeConfigMode,
@@ -16,6 +17,7 @@ const { getPonytailInstructions, filterSkillBodyForMode } = require("../hooks/po
1617

1718
export { filterSkillBodyForMode };
1819
export const readDefaultMode = getDefaultMode;
20+
export const readQuietStartup = getQuietStartup;
1921

2022
const RUNTIME_MODE_LIST = RUNTIME_MODES.join("|");
2123
const PONYTAIL_COMMAND_DESCRIPTION = `Set mode: ${RUNTIME_MODE_LIST}. Commands: status, default <mode>`;
@@ -178,7 +180,9 @@ export default function ponytailExtension(pi) {
178180
hideStatus = getHideStatus();
179181
currentMode = resolveSessionMode(entries, configuredDefaultMode);
180182
syncStatus(ctx);
181-
ctx?.ui?.notify?.(`Ponytail loaded: ${currentMode}`, "info");
183+
if (!getQuietStartup()) {
184+
ctx?.ui?.notify?.(`Ponytail loaded: ${currentMode}`, "info");
185+
}
182186
});
183187

184188
pi.on("agent_start", async (_event, ctx) => {

‎pi-extension/test/helpers.test.js‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from "node:assert/strict";
2-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import test from "node:test";
@@ -8,6 +8,7 @@ import {
88
filterSkillBodyForMode,
99
parsePonytailCommand,
1010
readDefaultMode,
11+
readQuietStartup,
1112
resolveSessionMode,
1213
writeDefaultMode,
1314
} from "../index.js";
@@ -61,6 +62,38 @@ test("readDefaultMode and writeDefaultMode use XDG config path", () => {
6162
}
6263
});
6364

65+
test("readQuietStartup resolves env var, config file, and default in that order", () => {
66+
const tempDir = mkdtempSync(join(tmpdir(), "ponytail-quiet-"));
67+
const previousXdg = process.env.XDG_CONFIG_HOME;
68+
const previousEnv = process.env.PONYTAIL_QUIET_STARTUP;
69+
const configDir = join(tempDir, "ponytail");
70+
const configPath = join(configDir, "config.json");
71+
process.env.XDG_CONFIG_HOME = tempDir;
72+
delete process.env.PONYTAIL_QUIET_STARTUP;
73+
74+
try {
75+
// No env, no config -> default false (toast still shows)
76+
assert.equal(readQuietStartup(), false);
77+
78+
// Config file true -> respected
79+
mkdirSync(configDir, { recursive: true });
80+
writeFileSync(configPath, JSON.stringify({ quietStartup: true }), "utf8");
81+
assert.equal(readQuietStartup(), true);
82+
83+
// Env var overrides config
84+
process.env.PONYTAIL_QUIET_STARTUP = "false";
85+
assert.equal(readQuietStartup(), false);
86+
process.env.PONYTAIL_QUIET_STARTUP = "1";
87+
assert.equal(readQuietStartup(), true);
88+
} finally {
89+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
90+
else process.env.XDG_CONFIG_HOME = previousXdg;
91+
if (previousEnv === undefined) delete process.env.PONYTAIL_QUIET_STARTUP;
92+
else process.env.PONYTAIL_QUIET_STARTUP = previousEnv;
93+
rmSync(tempDir, { recursive: true, force: true });
94+
}
95+
});
96+
6497
test("filterSkillBodyForMode keeps only requested intensity examples and rows", () => {
6598
const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n| **ultra** | keep ultra |\n- lite: Lite example\n- full: Full example\n- ultra: Ultra example\nOther line`;
6699

0 commit comments

Comments
 (0)