Files
boocode/apps/web/src/components/SessionLandingPage.tsx
indifferentketchup 23a33e893a 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>
2026-05-29 14:38:39 +00:00

83 lines
2.6 KiB
TypeScript

import { useCallback, useState } from 'react';
import { toast } from 'sonner';
import { ChatInput } from '@/components/ChatInput';
interface Props {
projectId: string;
sessionId: string;
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 }>;
}
export function SessionLandingPage({
projectId,
sessionId,
agentId,
onAgentChange,
onSend,
onSkillInvoke,
createChat,
}: Props) {
const [chatId, setChatId] = useState<string | null>(null);
const ensureChat = useCallback(async (): Promise<string> => {
if (chatId) return chatId;
try {
const chat = await createChat();
setChatId(chat.id);
return chat.id;
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to create chat');
throw err;
}
}, [chatId, createChat]);
const handleSend = useCallback(async (content: string) => {
const text = content.trim();
if (!text) return;
try {
await ensureChat();
onSend(text);
} catch {
// Error already surfaced via toast.
}
}, [ensureChat, onSend]);
// 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">
<div className="flex-1 flex items-center justify-center px-6">
<p className="text-sm text-muted-foreground">
Send a message to start.
</p>
</div>
<ChatInput
disabled={false}
projectId={projectId}
sessionId={sessionId}
agentId={agentId ?? null}
onAgentChange={onAgentChange}
onSend={handleSend}
onSlashCommand={handleSlashCommand}
chatId={chatId ?? undefined}
chatLabel="Chat"
messages={[]}
modelContextLimit={null}
/>
</div>
);
}