web+coder: segmented per-agent slash menu (agent commands + skills) + cross-agent skill execution

Coder / menu now shows two groups: the active agent's commands first (manifest + live ACP available_commands), BooCoder skills second. SlashCommandPicker gains an opt-in groups prop (flat items path unchanged -> BooChat byte-identical, parity verified); ChatInput takes slashGroups; CoderPane builds the groups. Skills run under the selected agent: coder skill_invoke accepts a provider and, when external, injects the server-side skill body into a dispatched task instead of native inference. Also folds in the initial-chat skill fix (handleLandingSkill: create chat -> assign to pane -> invoke, same transition as a text send) that resolves the landing-page blank screen. BooChat slash menu + skill invocation unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 14:38:39 +00:00
parent 8bf86ecb92
commit 23a33e893a
9 changed files with 232 additions and 62 deletions

View File

@@ -510,6 +510,31 @@ export function CoderPane({
[displayedCommands],
);
// v2.5.9: segmented slash menu — the active agent's commands first, then
// BooCoder skills. boocode has no separate "commands" group (it IS native),
// so it shows only Skills. Empty groups are dropped.
const agentCommands = useMemo(
() =>
agentConfig.provider === 'boocode'
? []
: mergeCommandsByName(providerCommands, liveTaskCommands),
[agentConfig.provider, providerCommands, liveTaskCommands],
);
const skillItems = useMemo(
() => skills.map((s) => ({ name: s.name, description: s.description })),
[skills],
);
const slashGroups = useMemo(() => {
const groups: Array<{ label: string; items: Array<{ name: string; description?: string }> }> = [];
if (agentCommands.length > 0) {
groups.push({ label: `${agentConfig.provider} commands`, items: agentCommands });
}
if (skillItems.length > 0) {
groups.push({ label: 'Skills', items: skillItems });
}
return groups;
}, [agentCommands, skillItems, agentConfig.provider]);
const { messages, setMessages, connected, loadMessages } = useCoderMessages(sessionId, chatId, {
onConnectedChange,
onPermissionRequested: (prompt) => {
@@ -736,19 +761,35 @@ export function CoderPane({
const handleChatInputSlash = useCallback(async (skillName: string, userMessage: string) => {
if (!chatId) return;
if (agentConfig.provider === 'boocode' && skillsByName.has(skillName)) {
setSending(true);
setPermissionPrompt(null);
setLiveTaskCommands([]);
try {
await api.coder.skillInvoke(sessionId, paneId, skillName, userMessage.length > 0 ? userMessage : null);
} catch (err) {
toast.error(err instanceof Error ? err.message : 'skill invocation failed');
} finally {
setSending(false);
}
// Only BooCoder skills route here; an agent's own commands (not skills) fall
// through to a literal send in ChatInput. Skills run under the active
// provider: boocode → native inference; external → body injected into a task.
if (!skillsByName.has(skillName)) return;
setSending(true);
setPermissionPrompt(null);
setLiveTaskCommands([]);
try {
const data = await api.coder.skillInvoke(
sessionId,
paneId,
skillName,
userMessage.length > 0 ? userMessage : null,
agentConfig.provider !== 'boocode'
? {
provider: agentConfig.provider,
model: agentConfig.model || undefined,
mode_id: agentConfig.modeId ?? undefined,
thinking_option_id: agentConfig.thinkingOptionId ?? undefined,
}
: undefined,
);
if (data.task_id) setActiveTaskId(data.task_id);
} catch (err) {
toast.error(err instanceof Error ? err.message : 'skill invocation failed');
} finally {
setSending(false);
}
}, [chatId, sessionId, paneId, agentConfig.provider, skillsByName]);
}, [chatId, sessionId, paneId, agentConfig, skillsByName]);
return (
<div className="flex flex-col h-full bg-background">
@@ -810,6 +851,7 @@ export function CoderPane({
projectId={projectPath ?? ''}
onSend={handleChatInputSend}
onSlashCommand={handleChatInputSlash}
slashGroups={slashGroups}
chatId={chatId ?? undefined}
chatLabel="BooCode"
messages={messages as unknown as import('@/api/types').Message[]}