fix(native-chat): keep Claude skills tagged as skills after the agent changes folder (#29030)

This commit is contained in:
Brennan Benson
2026-10-15 13:44:18 -07:00
committed by GitHub
parent 6fcd521165
commit 4fdccda051
2 changed files with 39 additions and 1 deletions
@@ -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: [] })
@@ -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<string>()
private commandNames = new Set<string>()
private details = new Map<string, CommandDetail>()
@@ -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<string, unknown>): 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)
)