This commit is contained in:
ZacharyZhang-NY committed 2026-03-18 14:42:14 -04:00
1 parent 1ba3147ed2
commit 0cd65521b8
32 files changed
+1853 -342

No files matched your search

+134 -38
View File
@@ -27,6 +27,7 @@ import type {
DepartmentId,
MessageChannel,
MessagePriority,
ProjectRecord,
TaskRecord,
} from "@shared/types"
import { getAgentInitials, formatMessageTime } from "../lib/formatters"
@@ -64,6 +65,8 @@ interface ChannelDescriptor {
targetId: string | null
/** Agent ID for DM channels */
agentId: string | null
/** Participant IDs for DM channels */
participantIds: string[]
}
interface TaskDiscussionDescriptor {
@@ -76,9 +79,11 @@ interface TaskDiscussionDescriptor {
function deriveChannels(
messages: AgentMessageRecord[],
agents: AgentRecord[],
projects?: ProjectRecord[],
): ChannelDescriptor[] {
const channelMap = new Map<string, ChannelDescriptor>()
const agentMap = new Map(agents.map((a) => [a.id, a]))
const projectMap = new Map((projects ?? []).map((project) => [project.id, project]))
// Always create company-wide channel
channelMap.set("company-all", {
@@ -88,6 +93,7 @@ function deriveChannels(
label: "#company-wide",
targetId: null,
agentId: null,
participantIds: [],
})
// Create permanent department channels for all departments that have agents
@@ -107,6 +113,7 @@ function deriveChannels(
label: `#${deptLabel}`,
targetId: dept,
agentId: null,
participantIds: [],
})
}
@@ -122,12 +129,14 @@ function deriveChannels(
if (!otherId) {
key = `direct-broadcast`
label = "Broadcast DMs"
agentId = null
} else {
const ids = [msg.fromAgentId, otherId].sort()
key = `direct-${ids.join("-")}`
const other = agentMap.get(otherId)
label = other ? other.name : "Unknown Agent"
agentId = otherId
const first = agentMap.get(ids[0] ?? "")
const second = agentMap.get(ids[1] ?? "")
label = `${first?.name ?? (ids[0] ? ids[0].slice(0, 8) : "Unknown")} ↔ ${second?.name ?? (ids[1] ? ids[1].slice(0, 8) : "Unknown")}`
agentId = ids[1] ?? null
}
break
}
@@ -145,13 +154,13 @@ function deriveChannels(
case "project": {
const projId = msg.channelTargetId ?? "general"
key = `project-${projId}`
label = `#project-${projId.slice(0, 8)}`
label = projectMap.get(projId)?.name ?? `#project-${projId.slice(0, 8)}`
break
}
case "incident": {
const incId = msg.channelTargetId ?? "general"
const incId = msg.channelTargetId ?? "room"
key = `incident-${incId}`
label = `#incident-${incId.slice(0, 8)}`
label = incId === "room" ? "#incident-room" : `#incident-${incId.slice(0, 8)}`
break
}
}
@@ -164,6 +173,9 @@ function deriveChannels(
label,
targetId: msg.channelTargetId,
agentId,
participantIds: msg.channel === "direct" && msg.toAgentId
? [msg.fromAgentId, msg.toAgentId].sort()
: [],
})
}
}
@@ -180,12 +192,17 @@ function deriveChannels(
function deriveTaskDiscussionChannels(
comments: CommentRecord[],
tasks: TaskRecord[],
forcedTaskId?: string | null,
): TaskDiscussionDescriptor[] {
const taskMap = new Map(tasks.map((task) => [task.id, task]))
const activeTaskIds = tasks
.filter((task) => ["todo", "in_progress", "in_review", "blocked"].includes(task.status))
.map((task) => task.id)
const discussionTaskIds = new Set([...comments.map((comment) => comment.taskId), ...activeTaskIds])
const discussionTaskIds = new Set([
...comments.map((comment) => comment.taskId),
...activeTaskIds,
...(forcedTaskId ? [forcedTaskId] : []),
])
return Array.from(discussionTaskIds)
.map((taskId) => {
@@ -227,9 +244,9 @@ function getChannelMessages(
}
if (channel.channel === "direct" && msg.channel === "direct") {
if (channel.key === "direct-broadcast") return !msg.toAgentId
if (!channel.agentId) return false
const ids = [msg.fromAgentId, msg.toAgentId ?? ""].sort()
return channel.key === `direct-${ids.join("-")}`
if (!msg.toAgentId || channel.participantIds.length < 2) return false
const ids = [msg.fromAgentId, msg.toAgentId].sort()
return ids[0] === channel.participantIds[0] && ids[1] === channel.participantIds[1]
}
return false
})
@@ -355,22 +372,12 @@ function MessageBubble({
isThread ? "ml-11" : ""
} ${
isUnread
? "border-l-[3px] bg-[color:var(--panel)]"
: "border-l-[3px] border-l-transparent hover:bg-[color:var(--panel-soft)]/40"
? "bg-[color:var(--panel)]"
: "hover:bg-[color:var(--panel-soft)]/40"
} ${
isUrgent
? "border-l-[color:var(--danger)]"
: isUnread
? `border-l-[${TERRACOTTA}]`
: ""
isUrgent ? "border-l-[3px]" : ""
}`}
style={
isUnread && !isUrgent
? { borderLeftColor: TERRACOTTA }
: isUrgent
? { borderLeftColor: "var(--danger)" }
: undefined
}
style={isUrgent ? { borderLeftColor: "var(--danger)" } : undefined}
>
<AgentAvatar agent={agent} />
@@ -610,6 +617,8 @@ interface MessageComposerProps {
/** Pre-fill channel context */
channel: MessageChannel
channelTargetId: string | null
/** Participant IDs for a direct channel */
directParticipantIds?: string[]
/** Pre-fill as a reply to a message */
replyTo: AgentMessageRecord | null
onSent: () => void
@@ -621,6 +630,7 @@ function MessageComposer({
companyId,
channel,
channelTargetId,
directParticipantIds = [],
replyTo,
onSent,
onCancelReply,
@@ -638,15 +648,41 @@ function MessageComposer({
}
}, [replyTo])
useEffect(() => {
if (agents.length === 0) {
if (fromAgentId) setFromAgentId("")
return
}
if (!agents.some((agent) => agent.id === fromAgentId)) {
setFromAgentId(agents[0]?.id ?? "")
}
}, [agents, fromAgentId])
const directRecipientId = useMemo(() => {
if (channel !== "direct") return null
if (replyTo?.fromAgentId) return replyTo.fromAgentId
if (directParticipantIds.length === 0) return null
if (directParticipantIds.length === 1) return directParticipantIds[0] ?? null
if (fromAgentId && directParticipantIds.includes(fromAgentId)) {
return directParticipantIds.find((agentId) => agentId !== fromAgentId) ?? null
}
return directParticipantIds[0] ?? null
}, [channel, replyTo, directParticipantIds, fromAgentId])
const handleSend = useCallback(async () => {
if (!body.trim() || !fromAgentId) return
if (channel === "direct" && !directRecipientId) return
if (!agents.some((agent) => agent.id === fromAgentId)) {
setSendError("Select a valid sender from the current company before sending.")
return
}
setSending(true)
setSendError(null)
try {
unwrap(await window.agentCompany.sendAgentMessage({
companyId,
fromAgentId,
toAgentId: replyTo ? replyTo.fromAgentId : null,
toAgentId: channel === "direct" ? directRecipientId : null,
channel,
channelTargetId,
subject: subject.trim() || (replyTo ? `Re: ${replyTo.subject}` : ""),
@@ -663,7 +699,7 @@ function MessageComposer({
} finally {
setSending(false)
}
}, [body, fromAgentId, companyId, channel, channelTargetId, replyTo, subject, onSent])
}, [agents, body, channel, channelTargetId, companyId, directRecipientId, fromAgentId, onSent, replyTo, subject])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -680,6 +716,11 @@ function MessageComposer({
return (
<div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
{sendError ? <div className="mb-3"><InlineNotice message={sendError} /></div> : null}
{channel === "direct" && !directRecipientId ? (
<div className="mb-2">
<InlineNotice message="Select a direct conversation partner before sending a DM." />
</div>
) : null}
<AnimatePresence>
{replyTo ? (
<motion.div
@@ -742,7 +783,7 @@ function MessageComposer({
<button
type="button"
onClick={() => void handleSend()}
disabled={!body.trim() || !fromAgentId || sending}
disabled={!body.trim() || !fromAgentId || sending || (channel === "direct" && !directRecipientId)}
className="flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-[8px] text-white transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none"
style={{ backgroundColor: TERRACOTTA }}
aria-label="Send message"
@@ -768,11 +809,12 @@ interface ComposeModalProps {
open: boolean
onClose: () => void
agents: AgentRecord[]
projects?: ProjectRecord[]
companyId: string
onSent: () => void
}
function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModalProps) {
function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: ComposeModalProps) {
const [fromAgentId, setFromAgentId] = useState(agents[0]?.id ?? "")
const [toAgentId, setToAgentId] = useState("")
const [channel, setChannel] = useState<MessageChannel>("company")
@@ -791,8 +833,38 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
return Array.from(depts).sort()
}, [agents])
useEffect(() => {
if (channel !== "direct") {
setToAgentId("")
}
if (channel !== "department" && channel !== "project") {
setChannelTargetId("")
}
}, [channel])
useEffect(() => {
if (agents.length === 0) {
if (fromAgentId) setFromAgentId("")
return
}
if (!agents.some((agent) => agent.id === fromAgentId)) {
setFromAgentId(agents[0]?.id ?? "")
}
}, [agents, fromAgentId])
const handleSend = useCallback(async () => {
if (!body.trim() || !fromAgentId || !subject.trim() || (channel === "direct" && !toAgentId)) return
const requiresTarget = channel === "department" || channel === "project"
if (
!body.trim()
|| !fromAgentId
|| !subject.trim()
|| (channel === "direct" && !toAgentId)
|| (requiresTarget && !channelTargetId)
) return
if (!agents.some((agent) => agent.id === fromAgentId)) {
setSendError("Select a valid sender from the current company before sending.")
return
}
setSending(true)
setSendError(null)
try {
@@ -801,7 +873,7 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
fromAgentId,
toAgentId: channel === "direct" && toAgentId ? toAgentId : null,
channel,
channelTargetId: channelTargetId || null,
channelTargetId: channel === "department" || channel === "project" ? channelTargetId || null : null,
subject: subject.trim(),
body: body.trim(),
priority,
@@ -818,7 +890,7 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
} finally {
setSending(false)
}
}, [body, fromAgentId, toAgentId, channel, channelTargetId, subject, priority, companyId, onSent, onClose])
}, [agents, body, channel, channelTargetId, companyId, fromAgentId, onClose, onSent, priority, subject, toAgentId])
return (
<Modal open={open} title="Compose Message" onClose={onClose} width="560px">
@@ -875,12 +947,18 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
})),
]}
/>
) : channel === "project" || channel === "incident" ? (
<Input
label={channel === "project" ? "Project ID" : "Incident ID"}
) : channel === "project" ? (
<Select
label="Project"
value={channelTargetId}
onChange={setChannelTargetId}
placeholder={`Enter ${channel} identifier`}
options={[
{ value: "", label: "Select project..." },
...(projects ?? []).map((p) => ({
value: p.id,
label: p.name,
})),
]}
/>
) : null}
@@ -921,7 +999,14 @@ function ComposeModal({ open, onClose, agents, companyId, onSent }: ComposeModal
<button
type="button"
onClick={() => void handleSend()}
disabled={!body.trim() || !subject.trim() || !fromAgentId || (channel === "direct" && !toAgentId) || sending}
disabled={
!body.trim()
|| !subject.trim()
|| !fromAgentId
|| (channel === "direct" && !toAgentId)
|| ((channel === "department" || channel === "project") && !channelTargetId)
|| sending
}
className="inline-flex items-center gap-2 rounded-[8px] px-4 py-2 text-[12px] font-semibold text-white transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none"
style={{ backgroundColor: TERRACOTTA }}
>
@@ -1084,6 +1169,7 @@ interface CommunicationHubProps {
comments: CommentRecord[]
tasks: TaskRecord[]
agents: AgentRecord[]
projects?: ProjectRecord[]
companyId: string
selectedEntityId?: string | null
onEntityConsumed?: () => void
@@ -1096,6 +1182,7 @@ export function CommunicationHub({
comments,
tasks,
agents,
projects,
companyId,
selectedEntityId,
onEntityConsumed,
@@ -1136,8 +1223,15 @@ export function CommunicationHub({
}, [effectiveMessages])
// Derive channels from messages
const messageChannels = useMemo(() => deriveChannels(effectiveMessages, agents), [effectiveMessages, agents])
const taskDiscussionChannels = useMemo(() => deriveTaskDiscussionChannels(comments, tasks), [comments, tasks])
const messageChannels = useMemo(() => deriveChannels(effectiveMessages, agents, projects), [effectiveMessages, agents, projects])
const forcedTaskDiscussionId = useMemo(
() => (selectedEntityId && tasks.some((task) => task.id === selectedEntityId) ? selectedEntityId : null),
[selectedEntityId, tasks],
)
const taskDiscussionChannels = useMemo(
() => deriveTaskDiscussionChannels(comments, tasks, forcedTaskDiscussionId),
[comments, forcedTaskDiscussionId, tasks],
)
const channels = useMemo(() => [...messageChannels, ...taskDiscussionChannels], [messageChannels, taskDiscussionChannels])
// Auto-select first valid channel
@@ -1633,6 +1727,7 @@ export function CommunicationHub({
companyId={companyId}
channel={activeChannel.channel}
channelTargetId={activeChannel.channel === "direct" ? null : activeChannel.targetId}
directParticipantIds={activeChannel.channel === "direct" ? activeChannel.participantIds : []}
replyTo={replyTo}
onSent={() => {
setReplyTo(null)
@@ -1662,6 +1757,7 @@ export function CommunicationHub({
open={showCompose}
onClose={() => setShowCompose(false)}
agents={agents}
projects={projects}
companyId={companyId}
onSent={onRefresh}
/>
+2 -2
View File
@@ -641,7 +641,7 @@ export function Dashboard(props: DashboardProps) {
value={liveRuns.length}
icon={Heartbeat}
tone={liveRuns.length > 0 ? "success" : "default"}
onClick={() => onNavigate("tasks")}
onClick={() => onNavigate("runs")}
subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} running` : "All quiet"}
/>
<MetricCard
@@ -665,7 +665,7 @@ export function Dashboard(props: DashboardProps) {
value={failedRuns.length}
icon={Warning}
tone={failedRuns.length > 0 ? "danger" : "default"}
onClick={() => onNavigate("tasks")}
onClick={() => onNavigate("runs")}
/>
<MetricCard
label="Total Spend"
+2 -2
View File
@@ -1486,8 +1486,8 @@ export function SettingsPane({
setActionNotice(null);
setLoadingAction("check");
try {
unwrap(await window.agentCompany.checkForUpdates());
setActionNotice("Update check started.");
const started = unwrap(await window.agentCompany.checkForUpdates());
setActionNotice(started ? "Update check started." : "Update check skipped because no release update feed is configured.");
} catch (error) {
setActionError(describeActionError(error));
} finally {
+50 -19
View File
@@ -148,6 +148,7 @@ export function AgentCreator({
const [capabilities, setCapabilities] = useState("");
const [reportsTo, setReportsTo] = useState("");
const [thinkingEffort, setThinkingEffort] = useState("");
const [requestError, setRequestError] = useState<string | null>(null);
const supportsEffort = connectorId === "claude_local" || connectorId === "codex_local";
useEffect(() => {
@@ -220,19 +221,25 @@ export function AgentCreator({
onChange={setCapabilities}
placeholder="What this agent is expected to do and when it becomes relevant."
/>
<p className="text-[12px] text-[color:var(--muted)]">
New agents enter the hiring pipeline first. Approval activates the agent and keeps onboarding, budgets, and org history linked to the same record.
</p>
<ActionButton
label="Create agent"
label="Submit hire request"
tone="accent"
onClick={() =>
void (async () => {
try {
setRequestError(null);
const meta: Record<string, string> = {};
if (thinkingEffort) meta.thinkingEffort = thinkingEffort;
await window.agentCompany.saveAgent({
unwrap(await window.agentCompany.requestHire({
companyId,
requestedByAgentId: null,
name,
role,
title,
department: null,
connectorId: connectorId as ConnectorRecord["id"],
workspaceId: workspaceId || null,
reportsTo: reportsTo || null,
@@ -240,21 +247,22 @@ export function AgentCreator({
metadataJson: JSON.stringify(meta),
capabilities,
budgetMonthlyUsd: Number(budget || 0),
status: "idle",
});
}));
setName("");
setRole("");
setTitle("");
setCapabilities("");
setReportsTo("");
setRequestError(null);
await onSaved();
} catch (error) {
void error;
setRequestError(error instanceof Error ? error.message : "Hire request failed.");
}
})()
}
icon={Robot}
/>
{requestError ? <p className="text-[12px] text-[color:var(--danger)]">{requestError}</p> : null}
</div>
);
}
@@ -292,6 +300,7 @@ export function AgentEditor({
const existingMeta = useMemo(() => { try { return JSON.parse(agent.metadataJson || "{}"); } catch { return {}; } }, [agent.metadataJson]);
const [thinkingEffort, setThinkingEffort] = useState<string>(existingMeta.thinkingEffort ?? "");
const supportsEffort = connectorId === "claude_local" || connectorId === "codex_local";
const pendingApproval = agent.status === "pending_approval";
useEffect(() => {
void (async () => {
@@ -314,27 +323,35 @@ export function AgentEditor({
return (
<div className="grid gap-4">
<Input label="Agent name" value={name} onChange={setName} placeholder="Agent name" />
<Input label="Role" value={role} onChange={setRole} placeholder="Role" />
<Input label="Title" value={title} onChange={setTitle} placeholder="Title" />
<Input label="Agent name" value={name} onChange={setName} placeholder="Agent name" readOnly={pendingApproval} disabled={pendingApproval} />
<Input label="Role" value={role} onChange={setRole} placeholder="Role" readOnly={pendingApproval} disabled={pendingApproval} />
<Input label="Title" value={title} onChange={setTitle} placeholder="Title" readOnly={pendingApproval} disabled={pendingApproval} />
<Select
label="Status"
value={status}
onChange={(v) => setStatus(v as AgentRecord["status"])}
options={[
{ value: "idle", label: "Idle" },
{ value: "active", label: "Active" },
{ value: "running", label: "Running" },
{ value: "paused", label: "Paused" },
{ value: "error", label: "Error" },
{ value: "pending_approval", label: "Pending Approval" },
{ value: "terminated", label: "Terminated" },
]}
disabled={pendingApproval}
options={pendingApproval
? [{ value: "pending_approval", label: "Pending Approval" }]
: [
{ value: "idle", label: "Idle" },
{ value: "active", label: "Active" },
{ value: "running", label: "Running" },
{ value: "paused", label: "Paused" },
{ value: "error", label: "Error" },
{ value: "terminated", label: "Terminated" },
]}
/>
{pendingApproval ? (
<p className="text-[12px] text-[color:var(--muted)]">
Pending hires must be approved or rejected from Hiring or Approvals. Status changes are intentionally locked here.
</p>
) : null}
<Select
label="Connector"
value={connectorId}
onChange={(v) => setConnectorId(v as ConnectorRecord["id"])}
disabled={pendingApproval}
options={connectors.map((c) => ({ value: c.id, label: c.label }))}
/>
{availableModels.length > 0 ? (
@@ -342,6 +359,7 @@ export function AgentEditor({
label="Model"
value={model}
onChange={setModel}
disabled={pendingApproval}
options={availableModels.map((m) => ({ value: m.id, label: m.label }))}
/>
) : null}
@@ -350,6 +368,7 @@ export function AgentEditor({
label="Thinking effort"
value={thinkingEffort}
onChange={setThinkingEffort}
disabled={pendingApproval}
options={[
{ value: "", label: "Default" },
{ value: "low", label: "Low" },
@@ -362,6 +381,7 @@ export function AgentEditor({
label="Workspace"
value={workspaceId}
onChange={setWorkspaceId}
disabled={pendingApproval}
options={[
{ value: "", label: "No workspace" },
...workspaces.map((w) => ({ value: w.id, label: w.name })),
@@ -371,13 +391,14 @@ export function AgentEditor({
label="Reports to"
value={reportsTo}
onChange={setReportsTo}
disabled={pendingApproval}
options={[
{ value: "", label: "No manager" },
...managerCandidates.map((a) => ({ value: a.id, label: a.name })),
]}
/>
<Input label="Monthly budget (USD)" value={budget} onChange={setBudget} placeholder="120" type="number" />
<TextArea label="Capabilities" value={capabilities} onChange={setCapabilities} placeholder="Capabilities" />
<Input label="Monthly budget (USD)" value={budget} onChange={setBudget} placeholder="120" type="number" readOnly={pendingApproval} disabled={pendingApproval} />
<TextArea label="Capabilities" value={capabilities} onChange={setCapabilities} placeholder="Capabilities" readOnly={pendingApproval} disabled={pendingApproval} />
{/* Heartbeat / Continuous Execution */}
<div className="rounded-lg border border-[color:var(--line)] p-3">
@@ -390,6 +411,7 @@ export function AgentEditor({
<input
type="checkbox"
checked={agent.heartbeatEnabled}
disabled={pendingApproval}
onChange={(e) =>
void (async () => {
await window.agentCompany.setHeartbeat({
@@ -427,6 +449,7 @@ export function AgentEditor({
await onSaved();
})()
}
disabled={pendingApproval}
options={[
{ value: "30", label: "30 seconds" },
{ value: "60", label: "1 minute" },
@@ -443,6 +466,7 @@ export function AgentEditor({
label="Wake now"
tone="accent"
icon={Lightning}
disabled={pendingApproval}
onClick={() =>
void (async () => {
await window.agentCompany.triggerHeartbeat({
@@ -462,6 +486,7 @@ export function AgentEditor({
label="Save changes"
tone="accent"
icon={Pencil}
disabled={pendingApproval}
onClick={() =>
void (async () => {
setSaveError(null);
@@ -507,10 +532,16 @@ export function AgentEditor({
label="Delete"
tone="danger"
icon={Trash}
disabled={pendingApproval}
onClick={() => setConfirmingDelete(true)}
/>
)}
</div>
{pendingApproval ? (
<p className="text-[12px] text-[color:var(--muted)]">
Pending hires are read-only here. Use Hiring or Approvals to approve or reject the request before editing the agent profile.
</p>
) : null}
{saveError ? <p className="text-[12px] text-[color:var(--danger)]">{saveError}</p> : null}
</div>
);
+11 -2
View File
@@ -208,19 +208,25 @@ export function TextArea({
placeholder,
rows = 4,
label,
readOnly = false,
disabled = false,
}: {
value: string;
onChange: (value: string) => void;
placeholder?: string;
rows?: number;
label?: string;
readOnly?: boolean;
disabled?: boolean;
}) {
const textarea = (
<textarea
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] disabled:cursor-not-allowed disabled:opacity-60"
value={value}
rows={rows}
placeholder={placeholder}
readOnly={readOnly}
disabled={disabled}
onChange={(event) => onChange(event.target.value)}
/>
);
@@ -241,12 +247,14 @@ export function Select({
options,
compact = false,
label,
disabled = false,
}: {
value: string;
onChange: (value: string) => void;
options: Array<{ value: string; label: string }>;
compact?: boolean;
label?: string;
disabled?: boolean;
}) {
const select = (
<select
@@ -254,8 +262,9 @@ export function Select({
compact
? "rounded-[6px] border-[color:var(--line)] px-2.5 py-1.5 text-[13px] font-medium"
: "rounded-[8px] border-[color:var(--line)] px-3.5 py-2.5 text-[14px]"
}`}
} disabled:cursor-not-allowed disabled:opacity-60`}
value={value}
disabled={disabled}
onChange={(event) => onChange(event.target.value)}
>
{options.map((option) => (