Skip to content

Commit ca48283

Browse files
authored
Merge pull request #1057 from jarvis24young/feat/reload-skills-command
feat: add /reload-skills slash command (CLI + Web UI)
2 parents 91b19c7 + fb7996c commit ca48283

6 files changed

Lines changed: 120 additions & 0 deletions

File tree

src/apps/cli/src/commands.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ pub const COMMAND_SPECS: &[CommandSpec] = &[
4444
name: "/skills",
4545
description: "List and configure skills",
4646
},
47+
CommandSpec {
48+
name: "/reload-skills",
49+
description: "Re-scan skill directories without restarting",
50+
},
4751
CommandSpec {
4852
name: "/subagents",
4953
description: "List and configure subagents",

src/apps/cli/src/modes/chat.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,9 @@ impl ChatMode {
17311731
"/skills" => {
17321732
self.show_skill_selector(chat_view, chat_state, rt_handle);
17331733
}
1734+
"/reload-skills" => {
1735+
self.reload_skills_from_disk(chat_view, chat_state, rt_handle);
1736+
}
17341737
"/subagents" => {
17351738
self.show_subagent_selector(chat_view, chat_state, rt_handle);
17361739
}
@@ -2883,6 +2886,44 @@ impl ChatMode {
28832886
chat_view.show_skill_menu();
28842887
}
28852888

2889+
/// Re-scan skill directories from disk and rebuild the registry cache.
2890+
///
2891+
/// Mirrors Claude Code 2.1.152 `/reload-skills`. Safe to call at any
2892+
/// time — does not require `is_processing` to be false because the
2893+
/// registry swap is atomic and a held `SkillInfo` reference is not
2894+
/// kept across the call.
2895+
fn reload_skills_from_disk(
2896+
&self,
2897+
chat_view: &mut ChatView,
2898+
chat_state: &mut ChatState,
2899+
rt_handle: &tokio::runtime::Handle,
2900+
) {
2901+
let registry = SkillRegistry::global();
2902+
let workspace = self.agent.workspace_path_buf();
2903+
let outcome = tokio::task::block_in_place(|| {
2904+
// refresh() is the global re-scan entry point; the workspace
2905+
// arg of refresh_for_workspace is currently a no-op upstream,
2906+
// so we call refresh() directly and re-resolve the workspace
2907+
// count afterwards.
2908+
rt_handle.block_on(async {
2909+
registry.refresh().await;
2910+
registry
2911+
.get_resolved_skills_for_workspace(Some(workspace.as_path()), None)
2912+
.await
2913+
})
2914+
});
2915+
2916+
let count = outcome.len();
2917+
chat_state.add_system_message(format!(
2918+
"Reloaded {} skill(s) from disk.",
2919+
count
2920+
));
2921+
chat_view.set_status(Some(format!(
2922+
"Skills reloaded ({} available)",
2923+
count
2924+
)));
2925+
}
2926+
28862927
fn show_available_skill_list(
28872928
&self,
28882929
chat_view: &mut ChatView,

src/web-ui/src/flow_chat/components/ChatInput.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,12 @@ export const ChatInput: React.FC<ChatInputProps> = ({
14491449
command: DEEP_REVIEW_SLASH_COMMAND,
14501450
label: t('chatInput.deepreviewAction'),
14511451
},
1452+
{
1453+
kind: 'action' as const,
1454+
id: 'reload-skills',
1455+
command: '/reload-skills',
1456+
label: t('chatInput.reloadSkillsAction'),
1457+
},
14521458
...(!derivedState?.isProcessing
14531459
? [
14541460
{
@@ -1949,6 +1955,47 @@ export const ChatInput: React.FC<ChatInputProps> = ({
19491955
threadGoalController,
19501956
]);
19511957

1958+
const submitReloadSkillsFromInput = useCallback(async () => {
1959+
const message = inputState.value.trim();
1960+
if (!/^\/reload-skills\s*$/i.test(message)) {
1961+
notificationService.warning(t('chatInput.reloadSkillsUsage'));
1962+
return;
1963+
}
1964+
1965+
dispatchInput({ type: 'CLEAR_VALUE' });
1966+
setQueuedInput(null);
1967+
setSlashCommandState({ isActive: false, kind: 'modes', query: '', selectedIndex: 0 });
1968+
1969+
try {
1970+
// Re-fetch skill configs with forceRefresh=true. The Tauri command
1971+
// (skill_api.rs::get_skill_configs) calls SkillRegistry::global().refresh()
1972+
// before serializing the result, so this single call both refreshes
1973+
// the registry cache and returns the new view. Pass workspacePath so
1974+
// workspace-level skills (`.bitfun/skills/`, `.cursor/skills/`, etc.)
1975+
// are included in the count — without it, the registry falls back
1976+
// to user + built-in slots only and the toast would undercount.
1977+
const skills = await configAPI.getSkillConfigs({
1978+
forceRefresh: true,
1979+
workspacePath: workspacePath || undefined,
1980+
});
1981+
notificationService.success(
1982+
t('chatInput.reloadSkillsDone', { count: skills.length }),
1983+
{ duration: 3000 }
1984+
);
1985+
} catch (error) {
1986+
log.error('Failed to trigger /reload-skills', { error });
1987+
dispatchInput({ type: 'ACTIVATE' });
1988+
dispatchInput({ type: 'SET_VALUE', payload: message });
1989+
notificationService.error(
1990+
error instanceof Error ? error.message : t('error.unknown'),
1991+
{
1992+
title: t('chatInput.reloadSkillsFailed'),
1993+
duration: 5000,
1994+
}
1995+
);
1996+
}
1997+
}, [inputState.value, setQueuedInput, t, workspacePath]);
1998+
19521999
const submitDeepreviewFromInput = useCallback(async () => {
19532000
if (!effectiveTargetSessionId || !effectiveTargetSession) {
19542001
notificationService.error(
@@ -2211,6 +2258,11 @@ export const ChatInput: React.FC<ChatInputProps> = ({
22112258
return;
22122259
}
22132260

2261+
if (localSlashCommandsEnabled && /^\/reload-skills\s*$/i.test(message)) {
2262+
await submitReloadSkillsFromInput();
2263+
return;
2264+
}
2265+
22142266
if (localSlashCommandsEnabled && resolveTypedMcpPromptCommand(message)) {
22152267
await submitMcpPromptFromInput();
22162268
return;
@@ -2243,6 +2295,11 @@ export const ChatInput: React.FC<ChatInputProps> = ({
22432295
);
22442296
return;
22452297
}
2298+
2299+
if (localSlashCommandsEnabled && message.toLowerCase().startsWith('/reload-skills')) {
2300+
notificationService.warning(t('chatInput.reloadSkillsUsage'));
2301+
return;
2302+
}
22462303

22472304
if (messageCharCount > CHAT_INPUT_CONFIG.largePaste.maxMessageChars) {
22482305
notificationService.error(
@@ -2310,6 +2367,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
23102367
submitInitFromInput,
23112368
submitDeepreviewFromInput,
23122369
submitMcpPromptFromInput,
2370+
submitReloadSkillsFromInput,
23132371
confirmPromptCacheGuardIfNeeded,
23142372
t,
23152373
resolveTypedMcpPromptCommand,
@@ -2420,6 +2478,11 @@ export const ChatInput: React.FC<ChatInputProps> = ({
24202478
next = '/init';
24212479
} else if (actionId === 'deepreview') {
24222480
next = `${DEEP_REVIEW_SLASH_COMMAND} `;
2481+
} else if (actionId === 'reload-skills') {
2482+
// /reload-skills takes no arguments. Setting the value to the bare
2483+
// command lets the user immediately press Enter to dispatch it
2484+
// (which is the same path /usage and /init use).
2485+
next = '/reload-skills';
24232486
} else {
24242487
return;
24252488
}

src/web-ui/src/locales/en-US/flow-chat.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,10 @@
595595
"deepreviewBusy": "A review is already running for this session. Stop or finish it before starting another Deep Review.",
596596
"deepreviewNestedDisabled": "Deep Review can only be started from the main session.",
597597
"deepreviewThreadTitle": "Deep review",
598+
"reloadSkillsAction": "Reload skills",
599+
"reloadSkillsUsage": "Use /reload-skills without extra arguments.",
600+
"reloadSkillsDone": "Reloaded skills ({{count}} available)",
601+
"reloadSkillsFailed": "Failed to reload skills",
598602
"currentMode": "Current mode: {{mode}}",
599603
"noMatchingMode": "No matching mode",
600604
"noMatchingCommand": "No matching command",

src/web-ui/src/locales/zh-CN/flow-chat.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,10 @@
589589
"deepreviewBusy": "当前会话已有审核正在进行,请停止或等待完成后再开始新的深度审核。",
590590
"deepreviewNestedDisabled": "深度审核只能从主会话启动。",
591591
"deepreviewThreadTitle": "深度审核",
592+
"reloadSkillsAction": "重新加载技能",
593+
"reloadSkillsUsage": "使用 /reload-skills 时不要带额外参数。",
594+
"reloadSkillsDone": "技能已重新加载(当前可用 {{count}} 个)",
595+
"reloadSkillsFailed": "技能重新加载失败",
592596
"currentMode": "当前模式: {{mode}}",
593597
"noMatchingMode": "没有匹配的模式",
594598
"noMatchingCommand": "没有匹配的命令",

src/web-ui/src/locales/zh-TW/flow-chat.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,10 @@
589589
"deepreviewBusy": "目前會話已有審核正在進行,請停止或等待完成後再開始新的深度審核。",
590590
"deepreviewNestedDisabled": "深度審核只能從主會話啟動。",
591591
"deepreviewThreadTitle": "深度審核",
592+
"reloadSkillsAction": "重新載入技能",
593+
"reloadSkillsUsage": "使用 /reload-skills 時不要帶額外參數。",
594+
"reloadSkillsDone": "技能已重新載入(目前可用 {{count}} 個)",
595+
"reloadSkillsFailed": "技能重新載入失敗",
592596
"currentMode": "目前模式: {{mode}}",
593597
"noMatchingMode": "沒有匹配的模式",
594598
"noMatchingCommand": "沒有匹配的命令",

0 commit comments

Comments
 (0)