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>
83 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|