"use client"; import { memo, useMemo } from "react"; import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { type OpenCodePermissionRequest, type OpenCodePermissionResponse, type OpenCodeQuestionRequest, type QuestionAnswer, useOpenCodeThreadState, } from "@assistant-ui/react-opencode"; import { CheckCircle2Icon, CircleHelpIcon, LoaderIcon, XCircleIcon, } from "lucide-react"; import { OpenCodePermissionCard } from "@/components/tools/opencode-permission-card"; import { OpenCodeQuestionCard, getQuestionSummary, } from "@/components/tools/opencode-question-card"; type ToolPermissionInteraction = { request: OpenCodePermissionRequest; state: "pending" | "resolved"; reply?: OpenCodePermissionResponse; }; type ToolQuestionInteraction = { request: OpenCodeQuestionRequest; state: "pending" | "answered" | "rejected"; answers?: readonly QuestionAnswer[]; }; const ASK_QUESTION_TOOL_NAMES = [ "ask_question", "request_user_input", "requestUserInput", ] as const; const useOpenCodeToolInteractions = (toolCallId: string) => { const interactions = useOpenCodeThreadState((state) => state.interactions); return useMemo(() => { const permissions: ToolPermissionInteraction[] = []; const questions: ToolQuestionInteraction[] = []; for (const request of Object.values(interactions.permissions.pending)) { if (request.tool?.callID === toolCallId) { permissions.push({ request, state: "pending" }); } } for (const resolved of Object.values(interactions.permissions.resolved)) { if (resolved.request.tool?.callID === toolCallId) { permissions.push({ request: resolved.request, state: "resolved", reply: resolved.reply, }); } } for (const request of Object.values(interactions.questions.pending)) { if (request.tool?.callID === toolCallId) { questions.push({ request, state: "pending" }); } } for (const answered of Object.values(interactions.questions.answered)) { if (answered.request.tool?.callID === toolCallId) { questions.push({ request: answered.request, state: "answered", answers: answered.answers, }); } } for (const rejected of Object.values(interactions.questions.rejected)) { if (rejected.request.tool?.callID === toolCallId) { questions.push({ request: rejected.request, state: "rejected", }); } } return { permissions, questions }; }, [interactions, toolCallId]); }; const ToolInteractionStack = ({ toolCallId }: { toolCallId: string }) => { const { permissions, questions } = useOpenCodeToolInteractions(toolCallId); if (permissions.length === 0 && questions.length === 0) { return null; } return (
{permissions.map((permission) => ( ))} {questions.map((question) => ( ))}
); }; export const withOpenCodeToolInteractions = ( BaseComponent: ToolCallMessagePartComponent, ): ToolCallMessagePartComponent => { const WrappedComponent: ToolCallMessagePartComponent = memo( (props) => { return (
); }, ); WrappedComponent.displayName = `withOpenCodeToolInteractions(${ BaseComponent.displayName || BaseComponent.name || "Tool" })`; return WrappedComponent; }; export const AskQuestionInline: ToolCallMessagePartComponent = memo( ({ toolCallId, toolName, status }) => { const { questions } = useOpenCodeToolInteractions(toolCallId); const primaryQuestion = questions[0]?.request; const label = getQuestionSummary(primaryQuestion); const statusType = status?.type ?? "complete"; return (
{statusType === "requires-action" ? ( ) : statusType === "running" ? ( ) : statusType === "incomplete" ? ( ) : ( )} {ASK_QUESTION_TOOL_NAMES.includes( toolName as (typeof ASK_QUESTION_TOOL_NAMES)[number], ) ? "ask_question" : toolName} {label}
); }, ); AskQuestionInline.displayName = "AskQuestionInline";