import { DropdownMenu } from "@astryxdesign/core/DropdownMenu"; import { Tooltip } from "@astryxdesign/core/Tooltip"; import { Check, ChevronDown, Folder, Plus, Send, Square, SlidersHorizontal, } from "lucide-react"; import { type FormEvent, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import type { WebSnapshot } from "../../../../protocol/types.ts"; import { workspaceName } from "../../lib/format.ts"; import type { WebStoreActions, WebStoreState } from "../../store/web-store.ts"; import { ActivityBar } from "../activity/ActivityBar.tsx"; interface ComposerProps { draftModel?: WebStoreState["draftModel"]; modelSelectionPending?: boolean; onInspect?: (terminalId?: string) => void; snapshot: WebSnapshot | null; selectedWorkspace: string | null; sessionSwitching: boolean; promptAdmissionPending: boolean; liveRunning: boolean; landing: boolean; actions: WebStoreActions; activeTurn: WebStoreState["activeTurn"]; turnCancellationPending: boolean; turnTerminalStatus: string | null; pendingFollowUpsReceipt: number | null; } export function Composer(props: ComposerProps) { const { t } = useTranslation(); const [prompt, setPrompt] = useState(""); const textarea = useRef(null); const selected = props.snapshot?.selectedSession; const active = Boolean( selected?.id && selected.id === props.snapshot?.currentSessionId, ); const draftSession = Boolean( props.selectedWorkspace && !selected && !props.snapshot?.currentSessionId, ); const canCompose = active || draftSession; const running = props.snapshot?.runtime.status === "running" || props.liveRunning; const canStop = active && running && Boolean(props.activeTurn ?? props.snapshot?.runtime.activeTurn); const disabled = props.sessionSwitching || (!canCompose && Boolean(props.selectedWorkspace)); const resize = (element: HTMLTextAreaElement) => { element.style.height = "auto"; element.style.height = `${Math.min(element.scrollHeight, 220)}px`; element.style.overflowY = element.scrollHeight > 220 ? "auto" : "hidden"; }; const send = async (event?: FormEvent) => { event?.preventDefault(); if (!props.selectedWorkspace) { await props.actions.chooseWorkspace(); return; } if (await props.actions.sendPrompt(prompt)) { setPrompt(""); if (textarea.current) { textarea.current.style.height = "auto"; textarea.current.style.overflowY = "hidden"; } } }; const workspaceItems = [ ...(props.snapshot?.workspaces ?? []).map((workspace) => ({ id: workspace.path, label: workspace.name, icon: , endContent: workspace.path === props.selectedWorkspace ? : undefined, onClick: () => props.actions.setWorkspace(workspace.path), })), { type: "divider" as const }, { id: "add", label: t("addWorkspaceMenu"), icon: , onClick: () => void props.actions.chooseWorkspace(), }, ]; const currentModel = props.draftModel ?? props.snapshot?.models.find((model) => model.current) ?? props.snapshot?.models[0]; const modelItems = (props.snapshot?.models ?? []).map((model) => ({ id: `${model.provider}/${model.id}`, label: model.label, endContent: ( props.draftModel ? props.draftModel.provider === model.provider && props.draftModel.id === model.id : model.current ) ? ( ) : undefined, onClick: () => void props.actions.selectModel(`${model.provider}/${model.id}`), })); const placeholder = !props.selectedWorkspace ? t("promptStart") : props.landing ? t("promptTask") : active ? t("promptMessage") : t("promptReadonly"); const hint = props.turnCancellationPending ? t("stoppingTurn") : props.turnTerminalStatus === "cancelled" ? t("stoppedTurn") : props.pendingFollowUpsReceipt !== null ? props.pendingFollowUpsReceipt > 0 ? t("pendingFollowUpsHint", { count: props.pendingFollowUpsReceipt }) : t("acceptedHint") : canCompose ? running ? t("queuedHint") : t("enterHint") : t("activeOnlyHint"); return (
{active && ( )} {props.landing && (
, size: "md", variant: "ghost", className: "workspace-picker", }} items={workspaceItems} menuWidth={240} placement="above" alignment="start" hasChevron />
)}
void send(event)} > {!props.selectedWorkspace && (