import {
ActionBarMorePrimitive,
ActionBarPrimitive,
AuiIf,
BranchPickerPrimitive,
ComposerPrimitive,
ErrorPrimitive,
MessagePrimitive,
SuggestionPrimitive,
ThreadPrimitive,
useAui,
useAuiState,
} from "@assistant-ui/react";
import {
ArrowDownIcon,
ArrowUpIcon,
BookOpenIcon,
BrainIcon,
CheckIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
DownloadIcon,
FileTextIcon,
ListIcon,
MoreHorizontalIcon,
PencilIcon,
RefreshCwIcon,
SquareIcon,
WrenchIcon,
} from "lucide-react";
import { Popover as PopoverPrimitive } from "radix-ui";
import type { ComponentProps, FC, KeyboardEvent, ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import { useActivityPanel } from "@/components/assistant-ui/activity-panel";
import {
ComposerAddAttachment,
ComposerAttachments,
UserMessageAttachments,
} from "@/components/assistant-ui/attachment";
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
import { Reasoning } from "@/components/assistant-ui/reasoning";
import {
fetchLatestRunStatus,
toReplayRepository,
} from "@/components/assistant-ui/thread-list";
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useClawSessionReplay } from "@/lib/claw-session-replay";
import { cn } from "@/lib/utils";
type ComposerInsertEvent = CustomEvent<{ text: string }>;
type SkillSummary = {
name: string;
description?: string;
when_to_use?: string;
};
type SlashCommandSummary = {
names: string[];
primary: string;
description?: string;
webui_supported?: boolean;
};
type ClawState = {
model?: string;
active_session_id?: string | null;
};
type ClawStoredSession = {
session_id?: string;
model?: string;
};
type ModelOption = {
id: string;
};
type ContextBudget = {
model?: string;
context_window_tokens?: number;
projected_input_tokens?: number;
message_tokens?: number;
chat_overhead_tokens?: number;
reserved_output_tokens?: number;
reserved_compaction_buffer_tokens?: number;
reserved_schema_tokens?: number;
hard_input_limit_tokens?: number;
soft_input_limit_tokens?: number;
overflow_tokens?: number;
soft_overflow_tokens?: number;
exceeds_hard_limit?: boolean;
exceeds_soft_limit?: boolean;
token_counter_backend?: string;
token_counter_source?: string;
token_counter_accurate?: boolean;
};
export const Thread: FC = () => {
useRefreshReplayedRun();
return (
s.thread.isEmpty}>
{() => }
);
};
function useRefreshReplayedRun() {
const { replaySession } = useClawSessionReplay();
const isRunning = useAuiState((s) => s.thread.isRunning);
const hasReplayedRunMessage = useAuiState((s) =>
s.thread.messages.some((message) => message.id.includes("-run-")),
);
const latestSessionId = useAuiState((s) =>
findLatestMessageMetadataValue(s.thread.messages, "sessionId"),
);
useEffect(() => {
if (!isRunning || !hasReplayedRunMessage) return;
const sessionId = normalizeSessionId(
latestSessionId ?? window.localStorage.getItem("claw.activeSessionId"),
);
if (!sessionId) return;
const activeSessionId = sessionId;
let cancelled = false;
async function refreshIfFinished() {
const runStatus = await fetchLatestRunStatus(activeSessionId);
if (cancelled) return;
if (runStatus?.status === "queued" || runStatus?.status === "running") {
return;
}
const response = await fetch(`/api/claw/sessions/${activeSessionId}`, {
cache: "no-store",
});
if (!response.ok || cancelled) return;
const payload = await response.json();
replaySession(
activeSessionId,
toReplayRepository(payload, activeSessionId, runStatus),
);
window.dispatchEvent(new Event("claw-sessions-changed"));
}
refreshIfFinished();
const interval = window.setInterval(refreshIfFinished, 3000);
return () => {
cancelled = true;
window.clearInterval(interval);
};
}, [hasReplayedRunMessage, isRunning, latestSessionId, replaySession]);
}
const ChatTopActions: FC = () => {
const { openFiles } = useActivityPanel();
const latestSessionId = useAuiState((s) =>
findLatestMessageMetadataValue(s.thread.messages, "sessionId"),
);
return (
);
};
const ThreadMessage: FC = () => {
const role = useAuiState((s) => s.message.role);
const isEditing = useAuiState((s) => s.message.composer.isEditing);
if (isEditing) return ;
if (role === "user") return ;
return ;
};
const ThreadScrollToBottom: FC = () => {
return (
);
};
const ThreadWelcome: FC = () => {
return (
);
};
const ThreadSuggestions: FC = () => {
return (
{() => }
);
};
const ThreadSuggestionItem: FC = () => {
return (
);
};
const Composer: FC = () => {
return (
);
};
const ComposerAction: FC = () => {
return (
!s.thread.isRunning}>
s.thread.isRunning}>
);
};
const ComposerContextStatus: FC = () => {
const status = useComposerContextStatus();
const totalTokens = status.contextBudget?.projected_input_tokens ?? 0;
const limit = status.contextBudget?.context_window_tokens ?? 0;
const percent = limit
? Math.min(100, Math.round((totalTokens / limit) * 100))
: 0;
return (
);
};
function ContextWindowButton({
percent,
totalTokens,
limit,
contextBudget,
}: {
percent: number;
totalTokens: number;
limit: number;
contextBudget?: ContextBudget;
}) {
const ringStyle = {
background: `conic-gradient(var(--primary) ${percent * 3.6}deg, var(--muted) 0deg)`,
};
return (
背景信息窗口:
{percent}% 已用(剩余 {Math.max(0, 100 - percent)}%)
已用 {formatCompactTokenCount(totalTokens)} 标记,共{" "}
{formatCompactTokenCount(limit)}
背景信息窗口:
{percent}% 已用(剩余 {Math.max(0, 100 - percent)}%)
已用 {formatCompactTokenCount(totalTokens)} 标记,共{" "}
{formatCompactTokenCount(limit)}
软阈值:
{formatCompactTokenCount(
contextBudget?.soft_input_limit_tokens ?? 0,
)}
硬阈值:
{formatCompactTokenCount(
contextBudget?.hard_input_limit_tokens ?? 0,
)}
保留输出:
{formatCompactTokenCount(
contextBudget?.reserved_output_tokens ?? 0,
)}
;压缩缓冲:
{formatCompactTokenCount(
contextBudget?.reserved_compaction_buffer_tokens ?? 0,
)}
计数器:{contextBudget?.token_counter_backend ?? "unknown"}
{contextBudget?.token_counter_accurate ? "(精确)" : "(估算)"}
);
}
function ModelPickerButton({
status,
}: {
status: {
model?: string;
models: ModelOption[];
setModel: (model: string) => Promise;
};
}) {
return (
切换模型
{status.models.length ? (
status.models.map((model) => (
))
) : (
模型列表读取中
)}
);
}
function useComposerContextStatus() {
const isRunning = useAuiState((s) => s.thread.isRunning);
const latestSessionId = useAuiState((s) =>
findLatestMessageMetadataValue(s.thread.messages, "sessionId"),
);
const wasRunningRef = useRef(false);
const [status, setStatus] = useState<{
model?: string;
contextBudget?: ContextBudget;
sessionId?: string | null;
models: ModelOption[];
setModel: (model: string) => Promise;
}>({
models: [],
setModel: async () => {},
});
useEffect(() => {
const cleanLatestSessionId = normalizeSessionId(latestSessionId);
if (cleanLatestSessionId) {
window.localStorage.setItem("claw.activeSessionId", cleanLatestSessionId);
window.setTimeout(() => {
window.dispatchEvent(new Event("claw-sessions-changed"));
}, 0);
}
}, [latestSessionId]);
useEffect(() => {
if (wasRunningRef.current && !isRunning) {
window.dispatchEvent(new Event("claw-sessions-changed"));
window.setTimeout(() => {
window.dispatchEvent(new Event("claw-sessions-changed"));
}, 1500);
}
wasRunningRef.current = isRunning;
}, [isRunning]);
useEffect(() => {
let cancelled = false;
async function refresh() {
try {
const stateResponse = await fetch("/api/claw/state", {
cache: "no-store",
});
const statePayload = stateResponse.ok
? ((await stateResponse.json()) as ClawState)
: {};
let sessionId = normalizeSessionId(
normalizeSessionId(latestSessionId) ||
normalizeSessionId(
window.localStorage.getItem("claw.activeSessionId"),
) ||
normalizeSessionId(statePayload.active_session_id),
);
if (sessionId)
window.localStorage.setItem("claw.activeSessionId", sessionId);
let sessionPayload: ClawStoredSession | null = null;
let contextBudget: ContextBudget | undefined;
if (sessionId) {
const sessionResponse = await fetch(
`/api/claw/sessions/${sessionId}`,
{
cache: "no-store",
},
);
if (sessionResponse.ok) {
sessionPayload =
(await sessionResponse.json()) as ClawStoredSession;
} else if (sessionResponse.status === 404) {
window.localStorage.removeItem("claw.activeSessionId");
sessionId = null;
}
}
const budgetUrl = new URL(
"/api/claw/context-budget",
window.location.origin,
);
if (sessionId) budgetUrl.searchParams.set("session_id", sessionId);
const budgetResponse = await fetch(budgetUrl, { cache: "no-store" });
contextBudget = budgetResponse.ok
? ((await budgetResponse.json()) as ContextBudget)
: undefined;
if (cancelled) return;
setStatus((current) => ({
model:
contextBudget?.model ?? sessionPayload?.model ?? statePayload.model,
contextBudget,
sessionId,
models: current.models,
setModel: current.setModel,
}));
} catch {
if (!cancelled) {
setStatus((current) => ({
...current,
}));
}
}
}
refresh();
const onRefresh = () => refresh();
window.addEventListener("focus", onRefresh);
window.addEventListener("claw-active-session-cleared", onRefresh);
const interval = window.setInterval(refresh, isRunning ? 2000 : 8000);
return () => {
cancelled = true;
window.removeEventListener("focus", onRefresh);
window.removeEventListener("claw-active-session-cleared", onRefresh);
window.clearInterval(interval);
};
}, [isRunning, latestSessionId]);
useEffect(() => {
let cancelled = false;
async function refreshModels() {
try {
const response = await fetch("/api/claw/models", {
cache: "no-store",
});
const payload = (await response.json()) as { models?: ModelOption[] };
if (!cancelled && response.ok) {
setStatus((current) => ({
...current,
models: Array.isArray(payload.models) ? payload.models : [],
}));
}
} catch {
if (!cancelled) {
setStatus((current) => ({ ...current, models: [] }));
}
}
}
refreshModels();
return () => {
cancelled = true;
};
}, []);
const setModel = useCallback(async (model: string) => {
const response = await fetch("/api/claw/state", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model }),
});
const payload = (await response.json()) as ClawState;
if (!response.ok) return;
setStatus((current) => ({
...current,
model: payload.model ?? model,
contextBudget: undefined,
}));
}, []);
useEffect(() => {
setStatus((current) => ({ ...current, setModel }));
}, [setModel]);
return status;
}
function findLatestMessageMetadataValue(
messages: readonly unknown[],
key: string,
) {
for (const message of [...messages].reverse()) {
if (!message || typeof message !== "object") continue;
const metadata = (message as { metadata?: unknown }).metadata;
const value = readMetadataValue(metadata, key);
if (value !== undefined) return value;
}
return undefined;
}
function readMetadataValue(metadata: unknown, key: string): unknown {
if (!metadata || typeof metadata !== "object") return undefined;
const record = metadata as Record;
if (record[key] !== undefined) return record[key];
const custom = record.custom;
if (custom && typeof custom === "object") {
return (custom as Record)[key];
}
return undefined;
}
function normalizeSessionId(value: unknown) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed || null;
}
function formatCompactTokenCount(value: number) {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
return `${value}`;
}
function compactModelLabel(model?: string) {
if (!model) return "模型";
const value = model.includes("/") ? model.split("/").at(-1) || model : model;
if (value.toLowerCase().startsWith("mimo-v2.5")) return "MiMo 2.5";
if (value.toLowerCase().startsWith("mimo-v2")) return "MiMo 2";
if (value.toLowerCase().startsWith("deepseek")) return "DeepSeek";
if (value.toLowerCase().startsWith("qwen")) return "Qwen";
if (value.toLowerCase().startsWith("minimax")) return "MiniMax";
return value;
}
function modelProvider(model: string) {
if (model.includes("/")) return model.split("/", 1)[0] || "unknown";
return "default";
}
const ComposerAssistButtons: FC = () => {
return (
}
triggerLabel=""
items={TOOL_PROMPTS}
/>
);
};
const TOOL_PROMPTS = [
{
title: "data_agent_profile_router_sessions",
description: "查看 parquet 线上 session 数据字段、规模和样例。",
prompt:
"请优先使用 data_agent_profile_router_sessions 了解我指定的 router_session_parquet 数据目录,再给出可用字段和初步挖掘建议。",
},
{
title: "data_agent_search_router_sessions",
description: "用关键词、正则、domain、device 等条件筛选候选。",
prompt:
"请优先使用 data_agent_search_router_sessions 按我的条件筛选线上候选,并说明筛选策略、命中数量和样例质量。",
},
{
title: "data_agent_normalize_dataset_draft",
description: "把自然语言 draft 转换为 canonical records。",
prompt:
"请在我确认数据内容后,使用 data_agent_normalize_dataset_draft 把 draft 转换成 canonical records。",
},
{
title: "data_agent_validate_dataset_records",
description: "校验 canonical records 是否符合结构约束。",
prompt:
"请使用 data_agent_validate_dataset_records 校验当前 canonical records,并解释每类错误应该如何修复。",
},
];
function PromptInsertDialog({
title,
description,
triggerIcon,
triggerLabel,
items,
}: {
title: string;
description: string;
triggerIcon: ReactNode;
triggerLabel: string;
items: { title: string; description: string; prompt: string }[];
}) {
const [open, setOpen] = useState(false);
return (
);
}
function SlashCommandDialog() {
const [open, setOpen] = useState(false);
const [commands, setCommands] = useState([]);
const [status, setStatus] = useState(
"点击后读取当前后端 slash command 列表。",
);
async function loadCommands() {
setStatus("正在读取 slash command 列表...");
try {
const response = await fetch("/api/claw/slash-commands", {
cache: "no-store",
});
const payload = (await response.json()) as
| SlashCommandSummary[]
| { error?: string };
if (!response.ok || !Array.isArray(payload)) {
throw new Error(
Array.isArray(payload) ? "读取失败" : (payload.error ?? "读取失败"),
);
}
setCommands(payload);
setStatus(payload.length ? "" : "当前没有可用 slash command。");
} catch (err) {
setStatus(err instanceof Error ? err.message : "读取 slash command 失败");
}
}
return (
);
}
function SkillInsertDialog() {
const [open, setOpen] = useState(false);
const [skills, setSkills] = useState([]);
const [status, setStatus] = useState("点击后读取当前后端 Skill 列表。");
async function loadSkills() {
setStatus("正在读取 Skill 列表...");
try {
const response = await fetch("/api/claw/skills", { cache: "no-store" });
const payload = (await response.json()) as
| SkillSummary[]
| { error?: string };
if (!response.ok || !Array.isArray(payload)) {
throw new Error(
Array.isArray(payload) ? "读取失败" : (payload.error ?? "读取失败"),
);
}
setSkills(payload);
setStatus(payload.length ? "" : "当前没有可用 Skill。");
} catch (err) {
setStatus(err instanceof Error ? err.message : "读取 Skill 失败");
}
}
return (
);
}
function insertComposerText(text: string) {
window.dispatchEvent(
new CustomEvent("claw-composer-insert", {
detail: { text },
}),
);
}
const MessageError: FC = () => {
return (
);
};
const AssistantMessage: FC = () => {
const ACTION_BAR_PT = "pt-1.5";
const ACTION_BAR_HEIGHT = `-mb-7.5 min-h-7.5 ${ACTION_BAR_PT}`;
const messageId = useAuiState((s) => s.message.id);
return (
{
if (part.type === "reasoning" || part.type === "tool-call")
return ["group-chain"];
return null;
}}
>
{({ part }) => {
switch (part.type) {
case "group-chain":
return (
);
case "text":
return (
);
case "reasoning":
return ;
case "tool-call":
return part.toolUI ?? ;
default:
return null;
}
}}
);
};
const ActivityChainSummary: FC<{
messageId: string;
partIndex: number | undefined;
}> = ({ messageId, partIndex }) => {
const { openItem } = useActivityPanel();
const startedAtRef = useRef(Date.now());
const [elapsedMs, setElapsedMs] = useState(0);
const label = useAuiState((s) => {
const message = s.message;
return summarizeActivityChainLabel(
message.content,
message.status?.type,
elapsedMs,
);
});
const active = useAuiState((s) => {
const message = s.message;
return message.status?.type === "running";
});
useEffect(() => {
if (!active) return;
setElapsedMs(Date.now() - startedAtRef.current);
const timer = window.setInterval(() => {
setElapsedMs(Date.now() - startedAtRef.current);
}, 1000);
return () => window.clearInterval(timer);
}, [active]);
return (
);
};
type ActivitySummaryPart = {
type: string;
text?: string;
result?: unknown;
args?: unknown;
toolName?: string;
};
function summarizeActivityChainLabel(
content: readonly ActivitySummaryPart[],
status: string | undefined,
elapsedMs = 0,
) {
const toolParts = content.filter((part) => part.type === "tool-call");
const runningTool = toolParts.findLast((part) => part.result === undefined);
const activeTool = runningTool ?? toolParts.at(-1);
const reasoningText =
content.find((part) => part.type === "reasoning")?.text ?? "";
const lastReasoningLine = reasoningText
.trim()
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.at(-1);
const stageNote = activeTool ? getToolStageNote(activeTool.args) : "";
const activeDetail =
stageNote ||
(activeTool?.toolName ? `调用 ${activeTool.toolName}` : "") ||
(isGenericActivityLine(lastReasoningLine) ? "" : lastReasoningLine);
if (status === "running") {
const elapsed = `用时 ${formatActivityDuration(elapsedMs)}`;
if (activeDetail) {
return `${compactActivityLabel(activeDetail, 15)},${elapsed}`;
}
return `思考中,${elapsed}`;
}
const label = compactActivityLabel(
activeDetail || lastReasoningLine || "思考完成",
15,
);
const finalDuration = extractDurationLabel(lastReasoningLine);
const elapsed = finalDuration || formatActivityDuration(elapsedMs);
return `${label},用时 ${elapsed}`;
}
function getToolStageNote(value: unknown) {
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
const note = (value as Record).__claw_stage_note;
return typeof note === "string" ? note.trim() : "";
}
function isGenericActivityLine(value?: string) {
if (!value) return true;
return (
value.startsWith("思考中") ||
value.startsWith("思考并执行完成") ||
value.endsWith("输出中...")
);
}
function extractDurationLabel(value?: string) {
if (!value) return "";
const match = value.match(/用时\s*([^。,.,\s]+)/);
return match?.[1] ?? "";
}
function formatActivityDuration(ms: number) {
const safeMs = Math.max(0, Math.round(ms));
if (safeMs < 1000) return `${safeMs}ms`;
const totalSeconds = Math.round(safeMs / 1000);
if (totalSeconds < 60) return `${totalSeconds}s`;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return seconds > 0 ? `${minutes}min${seconds}s` : `${minutes}min`;
}
function compactActivityLabel(label: string, maxLength = 15) {
const normalized = label.replace(/\s+/g, " ").trim();
if (normalized.length <= maxLength) return normalized;
return `${normalized.slice(0, Math.max(1, maxLength - 1))}…`;
}
const AssistantActionBar: FC = () => {
return (
s.message.isCopied}>
!s.message.isCopied}>
Export as Markdown
);
};
const UserMessage: FC = () => {
return (
);
};
const UserActionBar: FC = () => {
return (
);
};
const EditComposer: FC = () => {
return (
);
};
const ImeComposerInput: FC> = ({
autoFocus,
onChange,
onCompositionEnd,
onCompositionStart,
onKeyDown,
disabled,
...props
}) => {
const aui = useAui();
const storeText = useAuiState((s) =>
s.composer.isEditing ? s.composer.text : "",
);
const runtimeDisabled = useAuiState(
(s) => s.thread.isDisabled || s.composer.dictation?.inputDisabled,
);
const [localText, setLocalText] = useState(storeText);
const textareaRef = useRef(null);
const isComposingRef = useRef(false);
const justSubmittedRef = useRef(false);
const isDisabled = runtimeDisabled || disabled;
useEffect(() => {
if (!isComposingRef.current) {
if (justSubmittedRef.current && storeText) return;
if (!storeText) justSubmittedRef.current = false;
setLocalText(storeText);
}
}, [storeText]);
useEffect(() => {
if (
!isComposingRef.current &&
localText !== storeText &&
aui.composer().getState().isEditing
) {
aui.composer().setText(localText);
}
}, [aui, localText, storeText]);
useEffect(() => {
if (!autoFocus || isDisabled) return;
const textarea = textareaRef.current;
if (!textarea) return;
textarea.focus({ preventScroll: true });
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}, [autoFocus, isDisabled]);
useEffect(() => {
const clearAfterSubmit = () => {
justSubmittedRef.current = true;
setLocalText("");
};
const unsubscribeComposer = aui.on("composer.send", clearAfterSubmit);
const unsubscribeRun = aui.on("thread.runStart", clearAfterSubmit);
return () => {
unsubscribeComposer();
unsubscribeRun();
};
}, [aui]);
const syncComposerText = useCallback(
(value: string) => {
if (!aui.composer().getState().isEditing) return;
aui.composer().setText(value);
},
[aui],
);
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
const handleInsert = (event: Event) => {
const insertText = (event as ComposerInsertEvent).detail?.text;
if (!insertText) return;
const nextText = [localText.trimEnd(), insertText.trim()]
.filter(Boolean)
.join("\n\n");
setLocalText(nextText);
syncComposerText(nextText);
requestAnimationFrame(() => {
textarea.focus({ preventScroll: true });
textarea.setSelectionRange(nextText.length, nextText.length);
});
};
window.addEventListener("claw-composer-insert", handleInsert);
return () =>
window.removeEventListener("claw-composer-insert", handleInsert);
}, [localText, syncComposerText]);
const handleKeyDown = (event: KeyboardEvent) => {
onKeyDown?.(event);
if (event.defaultPrevented || isDisabled) return;
if (
event.nativeEvent.isComposing ||
isComposingRef.current ||
event.key !== "Enter" ||
event.shiftKey
) {
return;
}
const threadState = aui.thread().getState();
if (threadState.isRunning && !threadState.capabilities.queue) return;
event.preventDefault();
syncComposerText(localText);
aui.composer().send();
justSubmittedRef.current = true;
setLocalText("");
};
return (
{
onChange?.(event);
const nextText = event.currentTarget.value;
justSubmittedRef.current = false;
setLocalText(nextText);
const isNativeComposing =
(event.nativeEvent as { isComposing?: boolean }).isComposing === true;
if (!isNativeComposing && !isComposingRef.current) {
syncComposerText(nextText);
}
}}
onCompositionStart={(event) => {
onCompositionStart?.(event);
isComposingRef.current = true;
}}
onCompositionEnd={(event) => {
onCompositionEnd?.(event);
isComposingRef.current = false;
const target = event.currentTarget;
queueMicrotask(() => {
const nextText = target.value;
setLocalText(nextText);
syncComposerText(nextText);
});
}}
onKeyDown={handleKeyDown}
/>
);
};
const BranchPicker: FC = ({
className,
...rest
}) => {
return (
/
);
};