) => {
if (
e.key === "Delete" ||
e.key === "Backspace" ||
e.key === "Enter" ||
e.key === " "
) {
e.preventDefault();
removeChip(id);
}
},
[removeChip],
);
const attachButtonDisabled = disabled || full;
const showStopButton = isStreaming && !!onStop;
const inputTextClasses = cn(
"w-full resize-none bg-transparent",
isHero
? "min-h-[78px] px-5 pb-2 pt-5 text-[15px] leading-6"
: "min-h-[50px] px-4 pb-1.5 pt-3 text-[13.5px] leading-5",
);
return (
);
}
function ComposerModelBadge({
label,
provider,
providerLabel,
isHero,
}: {
label: string;
provider?: string | null;
providerLabel?: string | null;
isHero: boolean;
}) {
const inferredProvider = provider || inferProviderFromModelName(label);
const brand = providerBrand(inferredProvider);
const [logoIndex, setLogoIndex] = useState(0);
const logoUrl = brand?.logoUrls[logoIndex];
const showLogo = !!logoUrl;
const title = providerLabel ? `${label} · ${providerLabel}` : label;
useEffect(() => setLogoIndex(0), [inferredProvider]);
return (
{showLogo ? (
setLogoIndex((index) => index + 1)}
/>
) : brand ? (
{brand.initials.slice(0, 2)}
) : (
)}
{label}
);
}
function ComposerCliMentionOverlay({
segments,
isHero,
className,
}: {
segments: CapabilityMentionSegment[];
isHero: boolean;
className: string;
}) {
return (
{segments.map((segment, index) => {
if (segment.kind === "text") {
return {segment.text};
}
if (segment.kind === "cli") return (
);
return (
);
})}
);
}
interface SlashCommandPaletteProps {
commands: SlashCommand[];
selectedIndex: number;
layout: SlashPaletteLayout;
isHero: boolean;
onHover: (index: number) => void;
onChoose: (command: SlashCommand) => void;
}
interface CliAppMentionPaletteProps {
candidates: MentionCandidate[];
selectedIndex: number;
layout: SlashPaletteLayout;
isHero: boolean;
onHover: (index: number) => void;
onChoose: (candidate: MentionCandidate) => void;
}
function ImageAspectMenu({
selected,
isHero,
onSelect,
}: {
selected: ImageAspectRatio;
isHero: boolean;
onSelect: (ratio: ImageAspectRatio) => void;
}) {
const { t } = useTranslation();
return (
{t("thread.composer.imageMode.aspectLabel")}
{IMAGE_ASPECT_RATIOS.map((ratio) => {
const label = t(`thread.composer.imageMode.aspect.${ratio.replace(":", "_")}`);
return (
);
})}
);
}
function CliAppMentionPalette({
candidates,
selectedIndex,
layout,
isHero,
onHover,
onChoose,
}: CliAppMentionPaletteProps) {
const { t } = useTranslation();
const listMaxHeight = Math.max(
0,
layout.maxHeight - SLASH_PALETTE_CHROME_PX,
);
return (
{t("thread.composer.mentions.label")}
{candidates.map((candidate, index) => {
const selected = index === selectedIndex;
const name = candidate.name;
const displayName = candidate.kind === "cli"
? candidate.app.display_name
: candidate.preset.display_name;
const typeLabel = candidate.kind === "cli"
? t("thread.composer.mentions.cliBadge")
: t("thread.composer.mentions.mcpBadge");
const ariaDescription = candidate.kind === "cli"
? t("thread.composer.mentions.cliDescription", { name })
: t("thread.composer.mentions.mcpDescription", { name });
return (
);
})}
);
}
function MentionCandidateLogo({
candidate,
selected,
}: {
candidate: MentionCandidate;
selected: boolean;
}) {
const [logoIndex, setLogoIndex] = useState(0);
const color = (candidate.kind === "cli"
? candidate.app.brand_color
: candidate.preset.brand_color) || "hsl(var(--primary))";
const rawLogoUrl = candidate.kind === "cli" ? candidate.app.logo_url : candidate.preset.logo_url;
const logoUrls = useMemo(() => logoFallbackUrls(rawLogoUrl), [rawLogoUrl]);
const logoUrl = logoUrls[logoIndex];
useEffect(() => setLogoIndex(0), [rawLogoUrl]);
if (logoUrl) {
return (
setLogoIndex((index) => index + 1)}
/>
);
}
return (
{candidate.kind === "cli"
? cliAppInitials(candidate.app)
: mcpPresetInitials(candidate.preset)}
);
}
function SlashCommandPalette({
commands,
selectedIndex,
layout,
isHero,
onHover,
onChoose,
}: SlashCommandPaletteProps) {
const { t } = useTranslation();
const listMaxHeight = Math.max(
0,
layout.maxHeight - SLASH_PALETTE_CHROME_PX,
);
return (
{t("thread.composer.slash.label")}
{commands.map((command, index) => {
const Icon = COMMAND_ICONS[command.icon] ?? CircleHelp;
const selected = index === selectedIndex;
const commandKey = slashCommandI18nKey(command.command);
const title = t(`thread.composer.slash.commands.${commandKey}.title`, {
defaultValue: command.title,
});
const description = t(`thread.composer.slash.commands.${commandKey}.description`, {
defaultValue: command.description,
});
return (
);
})}
{t("thread.composer.slash.navigateHint")}
{t("thread.composer.slash.selectHint")}
{t("thread.composer.slash.closeHint")}
);
}
interface AttachmentChipProps {
image: AttachedImage;
labelRemove: string;
labelEncoding: string;
normalizedHint: (origBytes: number, currentBytes: number) => string;
formatError: (reason: AttachmentError) => string;
onRemove: () => void;
onKeyDown: (e: ReactKeyboardEvent) => void;
registerRef: (el: HTMLButtonElement | null) => void;
}
function AttachmentChip({
image,
labelRemove,
labelEncoding,
normalizedHint,
formatError,
onRemove,
onKeyDown,
registerRef,
}: AttachmentChipProps) {
const sizeLabel =
image.status === "ready" && image.normalized && image.encodedBytes
? normalizedHint(image.file.size, image.encodedBytes)
: formatBytes(image.file.size);
const tone =
image.status === "error"
? "border-destructive/40 bg-destructive/5 text-destructive"
: "border-border/70 bg-muted/60";
return (
{image.previewUrl ? (

) : (
)}
{image.status === "encoding" ? (
) : null}
{image.file.name}
{image.status === "error" && image.error
? formatError(image.error)
: sizeLabel}
);
}