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

@@ -1,6 +1,5 @@
import { useCallback, useState } from 'react';
import { toast } from 'sonner';
import { api } from '@/api/client';
import { ChatInput } from '@/components/ChatInput';
interface Props {
@@ -9,6 +8,10 @@ interface Props {
agentId?: string | null;
onAgentChange?: (agentId: string | null) => void | Promise<void>;
onSend: (content: string) => void;
// Slash-command (skill) send from the landing page. The parent creates the
// chat, assigns it to the pane (so it transitions to ChatPane), and invokes
// the skill — same transition the text send uses. See useSessionChats.
onSkillInvoke: (skillName: string, userMessage: string | null) => void;
createChat: () => Promise<{ id: string }>;
}
@@ -18,6 +21,7 @@ export function SessionLandingPage({
agentId,
onAgentChange,
onSend,
onSkillInvoke,
createChat,
}: Props) {
const [chatId, setChatId] = useState<string | null>(null);
@@ -45,14 +49,13 @@ export function SessionLandingPage({
}
}, [ensureChat, onSend]);
const handleSlashCommand = useCallback(async (skillName: string, userMessage: string) => {
try {
const cid = await ensureChat();
await api.chats.skillInvoke(cid, skillName, userMessage.length > 0 ? userMessage : null);
} catch (err) {
toast.error(err instanceof Error ? err.message : `/${skillName} failed`);
}
}, [ensureChat]);
// Route to the parent, which creates the chat, assigns it to the pane (so the
// pane transitions to ChatPane and subscribes to the stream), then invokes the
// skill — mirroring the text-send transition. Doing the skill invoke locally
// (without the pane assignment) left the landing pane stuck/blank.
const handleSlashCommand = useCallback((skillName: string, userMessage: string) => {
onSkillInvoke(skillName, userMessage.length > 0 ? userMessage : null);
}, [onSkillInvoke]);
return (
<div className="flex flex-col h-full min-h-0">