diff --git a/src/main/claude/claude-slash-command-catalog.test.ts b/src/main/claude/claude-slash-command-catalog.test.ts index 17d7fba8650..885d9fc90af 100644 --- a/src/main/claude/claude-slash-command-catalog.test.ts +++ b/src/main/claude/claude-slash-command-catalog.test.ts @@ -57,6 +57,25 @@ describe('claude slash command catalog', () => { ]) }) + it('keeps a project skill a skill after the agent cds out of the launch folder', () => { + // Captured on 2.1.280: after `cd` into an --add-dir, `skills` drops the launch folder's + // project skills while `slash_commands` still lists them. + const catalog = new ClaudeSlashCommandCatalog( + init({ cwd: '/repo', slash_commands: ['qa-project'], skills: ['qa-project'] }) + ) + catalog.observe( + init({ + cwd: '/scratch', + slash_commands: ['qa-project', 'found-here'], + skills: ['found-here'] + }) + ) + expect(catalog.commands).toEqual([ + { name: 'qa-project', kind: 'skill' }, + { name: 'found-here', kind: 'skill' } + ]) + }) + it('notices a name that only changed kind', () => { const catalog = new ClaudeSlashCommandCatalog( init({ slash_commands: ['review'], skills: [], terminal_slash_commands: [] }) diff --git a/src/main/claude/claude-slash-command-catalog.ts b/src/main/claude/claude-slash-command-catalog.ts index 2a4d91260f0..141b2847592 100644 --- a/src/main/claude/claude-slash-command-catalog.ts +++ b/src/main/claude/claude-slash-command-catalog.ts @@ -105,6 +105,8 @@ export function readClaudeSlashCommands( export class ClaudeSlashCommandCatalog { private entries: AgentSessionSlashCommand[] | undefined private hasSkillClassification = false + /** The folder the first catalog frame reported: the one the session's own `/` list was loaded for. */ + private launchCwd: string | undefined private hidden = new Set() private commandNames = new Set() private details = new Map() @@ -136,6 +138,23 @@ export class ClaudeSlashCommandCatalog { return this.entries } + /** Why: the CLI lists `skills` for the frame's `cwd`, which follows the agent's `cd`, but + * `slash_commands` for the session, so only a launch-folder frame may change a known kind. */ + private classify(message: Record): AgentSessionSlashCommand[] { + const reported = readClaudeSlashCommands(message) + const cwd = typeof message.cwd === 'string' ? message.cwd : undefined + this.launchCwd ??= cwd + if (cwd === undefined || cwd === this.launchCwd) { + return reported + } + const known = new Map( + (this.entries ?? []) + .filter((entry) => !entry.kindUnspecified) + .map((entry) => [entry.name, entry.kind]) + ) + return reported.map((entry) => ({ ...entry, kind: known.get(entry.name) ?? entry.kind })) + } + /** Provider row text, carried across the name-only frames that never restate it. */ private describe(entries: AgentSessionSlashCommand[]): AgentSessionSlashCommand[] { return entries.map((entry) => ({ ...entry, ...this.details.get(entry.name) })) @@ -147,7 +166,7 @@ export class ClaudeSlashCommandCatalog { if (carriesCommandCatalog(message)) { this.hasSkillClassification = true this.hidden = new Set(names(message.terminal_slash_commands)) - next = this.describe(readClaudeSlashCommands(message)) + next = this.describe(this.classify(message)) this.commandNames = new Set( next.filter((entry) => entry.kind === 'command').map((entry) => entry.name) )