Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions hooks/ponytail-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -98,5 +107,6 @@ module.exports = {
normalizeMode,
normalizeConfigMode,
normalizePersistedMode,
isDeactivationCommand,
writeDefaultMode,
};
4 changes: 2 additions & 2 deletions hooks/ponytail-mode-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down Expand Up @@ -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');
}
Expand Down
3 changes: 2 additions & 1 deletion pi-extension/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {
normalizeMode,
normalizeConfigMode,
normalizePersistedMode,
isDeactivationCommand,
writeDefaultMode,
} = require("../hooks/ponytail-config.js");
const { getPonytailInstructions, filterSkillBodyForMode } = require("../hooks/ponytail-instructions.js");
Expand Down Expand Up @@ -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");
}
});
Expand Down
12 changes: 12 additions & 0 deletions pi-extension/test/extension.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
}));
17 changes: 17 additions & 0 deletions tests/hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading