Skip to content

Commit 388b129

Browse files
authored
fix: only deactivate on a standalone "stop ponytail" / "normal mode" (DietrichGebert#162)
The deactivation check matched the phrase anywhere in the prompt, so an ordinary request like "add a normal mode toggle" silently turned ponytail off for the rest of the session. Match the whole message instead (trimmed, case-insensitive, trailing punctuation ignored) through a shared helper used by both the Claude/Codex hook and the pi extension. Fixes DietrichGebert#161
1 parent b5d8772 commit 388b129

5 files changed

Lines changed: 43 additions & 3 deletions

File tree

‎hooks/ponytail-config.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ function normalizePersistedMode(mode) {
3333
return normalizeMode(mode) || normalizeConfigMode(mode);
3434
}
3535

36+
// "stop ponytail" / "normal mode" turn ponytail off, but only as a standalone
37+
// command. Matching the phrase anywhere in the message turned it off mid-task
38+
// for ordinary requests like "add a normal mode toggle" — so require the whole
39+
// message to be the command, ignoring case and trailing punctuation.
40+
function isDeactivationCommand(text) {
41+
const t = String(text || '').trim().toLowerCase().replace(/[.!?\s]+$/, '');
42+
return t === 'stop ponytail' || t === 'normal mode';
43+
}
44+
3645
function getConfigDir() {
3746
if (process.env.XDG_CONFIG_HOME) {
3847
return path.join(process.env.XDG_CONFIG_HOME, 'ponytail');
@@ -98,5 +107,6 @@ module.exports = {
98107
normalizeMode,
99108
normalizeConfigMode,
100109
normalizePersistedMode,
110+
isDeactivationCommand,
101111
writeDefaultMode,
102112
};

‎hooks/ponytail-mode-tracker.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// ponytail — UserPromptSubmit hook to track which ponytail mode is active
33
// Inspects user input for /ponytail commands and writes mode to flag file
44

5-
const { getDefaultMode } = require('./ponytail-config');
5+
const { getDefaultMode, isDeactivationCommand } = require('./ponytail-config');
66
const { clearMode, setMode, writeHookOutput } = require('./ponytail-runtime');
77

88
let input = '';
@@ -45,7 +45,7 @@ process.stdin.on('end', () => {
4545
}
4646

4747
// Detect deactivation
48-
if (/\b(stop ponytail|normal mode)\b/i.test(prompt)) {
48+
if (isDeactivationCommand(prompt)) {
4949
clearMode();
5050
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF');
5151
}

‎pi-extension/index.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
normalizeMode,
88
normalizeConfigMode,
99
normalizePersistedMode,
10+
isDeactivationCommand,
1011
writeDefaultMode,
1112
} = require("../hooks/ponytail-config.js");
1213
const { getPonytailInstructions, filterSkillBodyForMode } = require("../hooks/ponytail-instructions.js");
@@ -133,7 +134,7 @@ export default function ponytailExtension(pi) {
133134
if (event?.source === "extension") return;
134135

135136
const text = String(event?.text || "");
136-
if (currentMode !== "off" && /\b(stop ponytail|normal mode)\b/i.test(text)) {
137+
if (currentMode !== "off" && isDeactivationCommand(text)) {
137138
setMode("off");
138139
}
139140
});

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,15 @@ test("normal mode disables persistent instructions", async () => withTempConfig(
121121
const disabled = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
122122
assert.equal(disabled, undefined);
123123
}));
124+
125+
test("a request mentioning normal mode stays active", async () => withTempConfig(async () => {
126+
const { commands, events } = createPiHarness();
127+
const ctx = createCommandContext();
128+
129+
await events.get("session_start")({ reason: "startup" }, ctx);
130+
await commands.get("ponytail").handler("ultra", ctx);
131+
await events.get("input")({ text: "add a normal mode toggle next to dark mode", source: "interactive" }, ctx);
132+
133+
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
134+
assert.match(result.systemPrompt, /PONYTAIL MODE ACTIVE/);
135+
}));

‎tests/hooks.test.js‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ assert.equal(fs.existsSync(codexState), false);
6464
output = JSON.parse(result.stdout);
6565
assert.equal(output.systemMessage, 'PONYTAIL:OFF');
6666

67+
// A request that merely mentions "normal mode" must not deactivate ponytail.
68+
result = run('ponytail-mode-tracker.js', codexEnv, JSON.stringify({ prompt: '@ponytail lite' }));
69+
assert.equal(result.status, 0, result.stderr);
70+
assert.equal(fs.readFileSync(codexState, 'utf8'), 'lite');
71+
72+
result = run(
73+
'ponytail-mode-tracker.js',
74+
codexEnv,
75+
JSON.stringify({ prompt: 'add a normal mode toggle next to dark mode' }),
76+
);
77+
assert.equal(result.status, 0, result.stderr);
78+
assert.equal(
79+
fs.readFileSync(codexState, 'utf8'),
80+
'lite',
81+
'incidental "normal mode" in a request must not turn ponytail off',
82+
);
83+
6784
const claudeEnv = {
6885
HOME: home,
6986
USERPROFILE: home,

0 commit comments

Comments
 (0)