Files
wehub-resource-sync e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

178 lines
5.3 KiB
TypeScript

"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 (
<div className="space-y-3">
{permissions.map((permission) => (
<OpenCodePermissionCard
key={`${permission.request.id}-${permission.state}`}
request={permission.request}
state={permission.state}
{...(permission.reply ? { reply: permission.reply } : {})}
/>
))}
{questions.map((question) => (
<OpenCodeQuestionCard
key={`${question.request.id}-${question.state}`}
request={question.request}
state={question.state}
{...(question.answers ? { answers: question.answers } : {})}
/>
))}
</div>
);
};
export const withOpenCodeToolInteractions = <TArgs = any, TResult = any>(
BaseComponent: ToolCallMessagePartComponent<TArgs, TResult>,
): ToolCallMessagePartComponent<TArgs, TResult> => {
const WrappedComponent: ToolCallMessagePartComponent<TArgs, TResult> = memo(
(props) => {
return (
<div className="space-y-3">
<BaseComponent {...props} />
<ToolInteractionStack toolCallId={props.toolCallId} />
</div>
);
},
);
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 (
<div className="text-muted-foreground flex items-center gap-2 py-0.5 text-sm">
{statusType === "requires-action" ? (
<CircleHelpIcon className="size-3.5 shrink-0" />
) : statusType === "running" ? (
<LoaderIcon className="size-3.5 shrink-0 animate-spin" />
) : statusType === "incomplete" ? (
<XCircleIcon className="text-destructive size-3.5 shrink-0" />
) : (
<CheckCircle2Icon className="size-3.5 shrink-0" />
)}
<span className="flex items-center gap-1.5 truncate">
<span className="font-medium">
{ASK_QUESTION_TOOL_NAMES.includes(
toolName as (typeof ASK_QUESTION_TOOL_NAMES)[number],
)
? "ask_question"
: toolName}
</span>
<span className="opacity-60">{label}</span>
</span>
</div>
);
},
);
AskQuestionInline.displayName = "AskQuestionInline";