diff --git a/hooks/ponytail-runtime.js b/hooks/ponytail-runtime.js index 37d682f..db0c477 100644 --- a/hooks/ponytail-runtime.js +++ b/hooks/ponytail-runtime.js @@ -4,13 +4,28 @@ const os = require('os'); const { getClaudeDir, getConfigDir } = require('./ponytail-config'); const STATE_FILE = '.ponytail-active'; -const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA); + +// ponytail: VS Code Copilot never sets COPILOT_PLUGIN_DATA — it only injects +// CLAUDE_PLUGIN_ROOT, pointed at an install path under .vscode/agent-plugins/ +// (#528). Without this fallback isCopilot was false, so ponytail assumed +// native Claude Code and emitted the statusline nudge, which VS Code Copilot +// doesn't read. +function isVsCodeCopilotRoot(pluginRoot) { + if (!pluginRoot) return false; + return pluginRoot.split(/[\\/]+/).includes('agent-plugins') && + pluginRoot.toLowerCase().includes('.vscode'); +} + +const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA) || + isVsCodeCopilotRoot(process.env.CLAUDE_PLUGIN_ROOT); const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA); const isQoder = !isCopilot && !isCodex && Boolean(process.env.QODER_SESSION_ID); let stateDir = getClaudeDir(); if (isCodex) stateDir = process.env.PLUGIN_DATA; -if (isCopilot) stateDir = process.env.COPILOT_PLUGIN_DATA; +// COPILOT_PLUGIN_DATA is unset under VS Code Copilot, so fall back to +// getClaudeDir() rather than building a path from undefined. +if (isCopilot) stateDir = process.env.COPILOT_PLUGIN_DATA || getClaudeDir(); if (isQoder) stateDir = path.join(os.homedir(), '.qoder'); const statePath = path.join(stateDir, STATE_FILE); diff --git a/tests/hooks.test.js b/tests/hooks.test.js index bf4a8eb..44ee922 100644 --- a/tests/hooks.test.js +++ b/tests/hooks.test.js @@ -198,6 +198,36 @@ assert.equal( output = JSON.parse(result.stdout); assert.match(output.additionalContext, /PONYTAIL MODE ACTIVE — level: full/); +// VS Code Copilot never sets COPILOT_PLUGIN_DATA — it only injects +// CLAUDE_PLUGIN_ROOT pointed at an agent-plugins/.../.vscode install path +// (#528). Without a fallback, isCopilot was false, so ponytail assumed +// native Claude Code and emitted the statusline nudge — noise, since VS +// Code Copilot doesn't read Claude's statusLine setting. +const vscodeHome = path.join(temp, 'vscode-copilot-home'); +const vscodePluginRoot = path.join( + vscodeHome, '.vscode', 'agent-plugins', 'github.com', 'DietrichGebert', 'ponytail', 'hooks', +); +fs.mkdirSync(vscodeHome, { recursive: true }); +result = run('ponytail-activate.js', { + HOME: vscodeHome, + USERPROFILE: vscodeHome, + CLAUDE_PLUGIN_ROOT: vscodePluginRoot, + PONYTAIL_DEFAULT_MODE: 'full', +}); +assert.equal(result.status, 0, result.stderr); +assert.ok( + !result.stdout.includes('STATUSLINE SETUP NEEDED'), + 'VS Code Copilot (detected via CLAUDE_PLUGIN_ROOT) must not get the Claude-only statusline nudge', +); +// isCopilot must still resolve a state dir even though COPILOT_PLUGIN_DATA +// is unset under VS Code — falling back to ~/.claude, not crashing on an +// undefined path. +assert.equal( + fs.readFileSync(path.join(vscodeHome, '.claude', '.ponytail-active'), 'utf8'), + 'full', + 'VS Code Copilot must persist mode state under getClaudeDir(), not a path built from the unset COPILOT_PLUGIN_DATA', +); + result = run( 'ponytail-mode-tracker.js', {