From 5c65d8520dfa669da8cae81add953eb90fd24a42 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 11 Aug 2026 16:17:22 -0500 Subject: [PATCH] fix(init): only show 'Restart your IDE' hint for IDE-embedded tools Reconstructed on current main (the original branch predated the Codex .agents rename, Command Code, Rovo Dev, Antigravity, Zoo Code, and the Kimi/Windsurf changes, so a direct rebase conflicted heavily in config.ts/init.ts/init.test.ts). Adds requiresIdeRestart to AIToolOption and gates the success-screen restart hint so it shows only when an IDE-resident tool actually received a surface. Wording follows that tool's own surface. Closes #1067. Co-Authored-By: Claude Opus 4.8 --- .changeset/restart-ide-hint.md | 7 ++++ src/core/config.ts | 31 ++++++++--------- src/core/init.ts | 33 ++++++++++++++---- test/core/init.test.ts | 61 ++++++++++++++++++++++++++++++++-- 4 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 .changeset/restart-ide-hint.md diff --git a/.changeset/restart-ide-hint.md b/.changeset/restart-ide-hint.md new file mode 100644 index 0000000000..b13a28127d --- /dev/null +++ b/.changeset/restart-ide-hint.md @@ -0,0 +1,7 @@ +--- +"@fission-ai/openspec": patch +--- + +### Bug Fixes + +- `openspec init` now suggests an IDE restart only when an IDE-resident tool such as Cursor, GitHub Copilot, Continue, or Cline was configured. CLI tools like Claude Code, Codex, and Gemini CLI no longer show the hint, since their commands work as soon as the files exist. diff --git a/src/core/config.ts b/src/core/config.ts index e8f7d20162..78dfac3074 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -34,45 +34,46 @@ export interface AIToolOption { globalSkillsDir?: string; // e.g., '.minimax' - /skills suffix, resolved from the user's home directory detectionPaths?: string[]; // Override skillsDir for auto-detection; any path existing triggers detection setupNote?: string; // Manual setup required before the tool picks up generated files; shown after init/update + requiresIdeRestart?: boolean; // True when slash commands are loaded by an IDE/editor process (a CLI picks them up immediately, so no restart hint — see #1067) } export const AI_TOOLS: AIToolOption[] = [ - { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq' }, - { name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent' }, + { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq', requiresIdeRestart: true }, + { name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent', requiresIdeRestart: true }, { name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' }, { name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' }, { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' }, - { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' }, + { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline', requiresIdeRestart: true }, { name: 'Command Code', value: 'command-code', available: true, successLabel: 'Command Code', skillsDir: '.commandcode' }, { name: 'CodeArts', value: 'codeartsagent', available: true, successLabel: 'CodeArts', skillsDir: '.codeartsdoer' }, { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.agents', legacySkillsDirs: ['.codex'], detectionPaths: ['.agents/skills', '.codex/skills'] }, - { name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'] }, + { name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'], requiresIdeRestart: true }, { name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' }, { name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' }, - { name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue' }, - { name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec' }, + { name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue', requiresIdeRestart: true }, + { name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec', requiresIdeRestart: true }, { name: 'Crush', value: 'crush', available: true, successLabel: 'Crush', skillsDir: '.crush' }, - { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor' }, + { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor', requiresIdeRestart: true }, { name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' }, { name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' }, - { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] }, + { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'], requiresIdeRestart: true }, { name: 'Hermes Agent', value: 'hermes', available: true, successLabel: 'Hermes Agent', skillsDir: '.hermes', detectionPaths: ['.hermes', 'HERMES.md', '.hermes.md'], setupNote: "Hermes only loads skills from ~/.hermes/skills by default. Add this project's .hermes/skills directory to skills.external_dirs in ~/.hermes/config.yaml so Hermes picks up the generated OpenSpec skills." }, { name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' }, - { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' }, - { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' }, + { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie', requiresIdeRestart: true }, + { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode', requiresIdeRestart: true }, { name: 'Kimi Code', value: 'kimi', available: true, successLabel: 'Kimi Code', skillsDir: '.kimi-code', detectionPaths: ['.kimi-code', '.kimi'] }, - { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' }, - { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' }, + { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro', requiresIdeRestart: true }, + { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma', requiresIdeRestart: true }, { name: 'MiniMax Code', value: 'minimax-code', available: true, successLabel: 'MiniMax Code', globalSkillsDir: '.minimax' }, { name: 'Mistral Vibe', value: 'vibe', available: true, successLabel: 'Mistral Vibe', skillsDir: '.vibe' }, { name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' }, { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' }, { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' }, - { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' }, + { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder', requiresIdeRestart: true }, { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' }, { name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] }, - { name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo' }, - { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' }, + { name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo', requiresIdeRestart: true }, + { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae', requiresIdeRestart: true }, { name: 'ZCode', value: 'zcode', available: true, successLabel: 'ZCode', skillsDir: '.zcode' }, // Vendor-neutral target for assistants that read the shared `.agents` root. // Detection keys off `.agents/skills` rather than the bare root: frameworks use diff --git a/src/core/init.ts b/src/core/init.ts index 76c2a96977..3684d6a04f 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -130,6 +130,7 @@ type ValidatedInitTool = { skillsRoot: string; isGlobalSkillTarget: boolean; wasConfigured: boolean; + requiresIdeRestart?: boolean; }; /** @@ -788,6 +789,7 @@ export class InitCommand { skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath, isGlobalSkillTarget, wasConfigured: preState?.configured ?? false, + requiresIdeRestart: tool.requiresIdeRestart, }); } @@ -1274,16 +1276,33 @@ export class InitCommand { console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`); console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`); - // Restart instruction if any tools were configured and got a surface - // (when nothing was generated there is nothing a restart would pick up); - // only mention commands when commands were actually generated. Not "slash - // commands": Amazon Q's generated files are prompt-library entries invoked - // with @, so a restart line promising slash commands would be wrong for it. - if ((results.createdTools.length > 0 || results.refreshedTools.length > 0) && (commandsGenerated || skillsGenerated)) { + // Restart instruction only when at least one IDE/editor-resident tool + // actually received a generated surface. Two conditions, coupled to the SAME + // tool: (1) its commands/skills are loaded by a long-running editor process + // (CLI tools pick the files up immediately, so a restart line would be wrong + // for them — see #1067), and (2) a surface was actually generated for it + // under the active delivery (an IDE tool that generated nothing has nothing a + // restart would pick up, even if a co-configured CLI tool did generate). + // Wording follows what the IDE tool itself generated, not the global + // aggregate: it must not say "commands" when the IDE tool only got skills + // while a co-configured CLI tool got commands. Not "slash commands" either: + // Amazon Q's generated files are prompt-library entries invoked with @, so a + // restart line promising slash commands would be wrong for it. + const restartCommandsGenerated = successfulTools.some( + (tool) => + tool.requiresIdeRestart && + shouldGenerateCommandsForTool(tool.value, activeDelivery) + ); + const restartSkillsGenerated = successfulTools.some( + (tool) => + tool.requiresIdeRestart && + shouldGenerateSkillsForTool(tool.value, activeDelivery) + ); + if (restartCommandsGenerated || restartSkillsGenerated) { console.log(); console.log( chalk.white( - commandsGenerated + restartCommandsGenerated ? 'Restart your IDE for the new commands to take effect.' : 'Restart your IDE for the new skills to take effect.' ) diff --git a/test/core/init.test.ts b/test/core/init.test.ts index d660648fe0..c73d1e7d74 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -759,6 +759,54 @@ describe('InitCommand', () => { expect(await fs.readFile(customSkill, 'utf-8')).toBe('user skill'); }); + it('should not suggest an IDE restart for CLI-only tools', async () => { + const initCommand = new InitCommand({ tools: 'claude', force: true }); + + await initCommand.execute(testDir); + + expect(getConsoleOutput()).not.toContain('Restart your IDE'); + }); + + it('should suggest an IDE restart for IDE-resident tools', async () => { + const initCommand = new InitCommand({ tools: 'cursor', force: true }); + + await initCommand.execute(testDir); + + expect(getConsoleOutput()).toContain('Restart your IDE'); + }); + + it('should suggest an IDE restart when a mix of CLI and IDE tools is configured', async () => { + // One IDE-resident tool (cursor) among CLI tools (claude) is enough: the + // hint targets the tool that needs it, so the gate must not require every + // configured tool to be IDE-resident. + const initCommand = new InitCommand({ tools: 'claude,cursor', force: true }); + + await initCommand.execute(testDir); + + expect(getConsoleOutput()).toContain('Restart your IDE'); + }); + + it('should word the restart hint for commands when an IDE tool gets a command surface', async () => { + // Default delivery generates commands for an adapter-backed IDE tool, so the + // hint must name commands, driven by the IDE tool's own generated surface. + const initCommand = new InitCommand({ tools: 'cursor', force: true }); + + await initCommand.execute(testDir); + + expect(getConsoleOutput()).toContain('Restart your IDE for the new commands to take effect.'); + }); + + it('should word the restart hint for skills when an IDE tool gets only a skill surface', async () => { + // Skills-only delivery generates no commands, so the same IDE tool must be + // told about skills, not commands. + saveGlobalConfig({ featureFlags: {}, profile: 'core', delivery: 'skills' }); + const initCommand = new InitCommand({ tools: 'cursor', force: true }); + + await initCommand.execute(testDir); + + expect(getConsoleOutput()).toContain('Restart your IDE for the new skills to take effect.'); + }); + it('should create skills for multiple tools at once', async () => { const initCommand = new InitCommand({ tools: 'claude,cursor', force: true }); @@ -1721,10 +1769,10 @@ describe('InitCommand - profile and detection features', () => { expect(startHint).not.toContain('/openspec-propose'); expect(startHint).not.toContain('/opsx:propose'); - // No slash commands were generated, so the restart line must not claim any + // Codex is a CLI tool: its skills load as soon as the files exist, with no + // IDE process to restart, so the restart line must not appear at all (#1067). const restartHint = logCalls.find((entry) => entry.includes('Restart your IDE')); - expect(restartHint).toContain('Restart your IDE for the new skills to take effect.'); - expect(restartHint).not.toContain('slash commands'); + expect(restartHint).toBeUndefined(); }); it('should print the @-prefixed prompt hint for amazon-q (prompt library, no slash surface)', async () => { @@ -1999,3 +2047,10 @@ async function directoryExists(dirPath: string): Promise { return false; } } + +function getConsoleOutput(): string { + return (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls + .flat() + .map(String) + .join('\n'); +}