diff --git a/hooks/ponytail-config.js b/hooks/ponytail-config.js index f32fb7c..a96d6d0 100644 --- a/hooks/ponytail-config.js +++ b/hooks/ponytail-config.js @@ -33,6 +33,15 @@ function normalizePersistedMode(mode) { return normalizeMode(mode) || normalizeConfigMode(mode); } +// "stop ponytail" / "normal mode" turn ponytail off, but only as a standalone +// command. Matching the phrase anywhere in the message turned it off mid-task +// for ordinary requests like "add a normal mode toggle" — so require the whole +// message to be the command, ignoring case and trailing punctuation. +function isDeactivationCommand(text) { + const t = String(text || '').trim().toLowerCase().replace(/[.!?\s]+$/, ''); + return t === 'stop ponytail' || t === 'normal mode'; +} + function getConfigDir() { if (process.env.XDG_CONFIG_HOME) { return path.join(process.env.XDG_CONFIG_HOME, 'ponytail'); @@ -98,5 +107,6 @@ module.exports = { normalizeMode, normalizeConfigMode, normalizePersistedMode, + isDeactivationCommand, writeDefaultMode, }; diff --git a/hooks/ponytail-mode-tracker.js b/hooks/ponytail-mode-tracker.js index d4fda46..5e85937 100644 --- a/hooks/ponytail-mode-tracker.js +++ b/hooks/ponytail-mode-tracker.js @@ -2,7 +2,7 @@ // ponytail — UserPromptSubmit hook to track which ponytail mode is active // Inspects user input for /ponytail commands and writes mode to flag file -const { getDefaultMode } = require('./ponytail-config'); +const { getDefaultMode, isDeactivationCommand } = require('./ponytail-config'); const { clearMode, setMode, writeHookOutput } = require('./ponytail-runtime'); let input = ''; @@ -45,7 +45,7 @@ process.stdin.on('end', () => { } // Detect deactivation - if (/\b(stop ponytail|normal mode)\b/i.test(prompt)) { + if (isDeactivationCommand(prompt)) { clearMode(); writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF'); } diff --git a/pi-extension/index.js b/pi-extension/index.js index 5d74798..b8a35e8 100644 --- a/pi-extension/index.js +++ b/pi-extension/index.js @@ -7,6 +7,7 @@ const { normalizeMode, normalizeConfigMode, normalizePersistedMode, + isDeactivationCommand, writeDefaultMode, } = require("../hooks/ponytail-config.js"); const { getPonytailInstructions, filterSkillBodyForMode } = require("../hooks/ponytail-instructions.js"); @@ -133,7 +134,7 @@ export default function ponytailExtension(pi) { if (event?.source === "extension") return; const text = String(event?.text || ""); - if (currentMode !== "off" && /\b(stop ponytail|normal mode)\b/i.test(text)) { + if (currentMode !== "off" && isDeactivationCommand(text)) { setMode("off"); } }); diff --git a/pi-extension/test/extension.test.js b/pi-extension/test/extension.test.js index 1c5f7c0..1e3817c 100644 --- a/pi-extension/test/extension.test.js +++ b/pi-extension/test/extension.test.js @@ -121,3 +121,15 @@ test("normal mode disables persistent instructions", async () => withTempConfig( const disabled = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx); assert.equal(disabled, undefined); })); + +test("a request mentioning normal mode stays active", async () => withTempConfig(async () => { + const { commands, events } = createPiHarness(); + const ctx = createCommandContext(); + + await events.get("session_start")({ reason: "startup" }, ctx); + await commands.get("ponytail").handler("ultra", ctx); + await events.get("input")({ text: "add a normal mode toggle next to dark mode", source: "interactive" }, ctx); + + const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx); + assert.match(result.systemPrompt, /PONYTAIL MODE ACTIVE/); +})); diff --git a/tests/hooks.test.js b/tests/hooks.test.js index 46e7be6..4f0d9b3 100644 --- a/tests/hooks.test.js +++ b/tests/hooks.test.js @@ -64,6 +64,23 @@ assert.equal(fs.existsSync(codexState), false); output = JSON.parse(result.stdout); assert.equal(output.systemMessage, 'PONYTAIL:OFF'); +// A request that merely mentions "normal mode" must not deactivate ponytail. +result = run('ponytail-mode-tracker.js', codexEnv, JSON.stringify({ prompt: '@ponytail lite' })); +assert.equal(result.status, 0, result.stderr); +assert.equal(fs.readFileSync(codexState, 'utf8'), 'lite'); + +result = run( + 'ponytail-mode-tracker.js', + codexEnv, + JSON.stringify({ prompt: 'add a normal mode toggle next to dark mode' }), +); +assert.equal(result.status, 0, result.stderr); +assert.equal( + fs.readFileSync(codexState, 'utf8'), + 'lite', + 'incidental "normal mode" in a request must not turn ponytail off', +); + const claudeEnv = { HOME: home, USERPROFILE: home,