Files
huskies/frontend/src/components/WorkItemDetailPanelHeader.tsx
T

185 lines
4.3 KiB
TypeScript

/** Header sub-component for WorkItemDetailPanel. */
import type { AgentConfigInfo, AgentInfo, AgentStatusValue } from "../api/agents";
import { STAGE_LABELS, formatStoryTitle } from "./workItemDetailPanelUtils";
const STAGE_TO_AGENT_STAGE: Record<string, string> = {
current: "coder",
qa: "qa",
merge: "mergemaster",
};
interface WorkItemDetailPanelHeaderProps {
storyId: string;
name: string | null;
stage: string;
assignedAgent: string | null;
agentConfig: AgentConfigInfo[];
agentInfo: AgentInfo | null;
agentStatus: AgentStatusValue | null;
assigning: boolean;
assignError: string | null;
onAgentAssign: (agentName: string) => Promise<void>;
onClose: () => void;
}
/**
* Panel header: title, stage label, agent assignment dropdown, and close button.
*/
export function WorkItemDetailPanelHeader({
storyId,
name,
stage,
assignedAgent,
agentConfig,
agentInfo,
agentStatus,
assigning,
assignError,
onAgentAssign,
onClose,
}: WorkItemDetailPanelHeaderProps) {
const stageLabel = STAGE_LABELS[stage] ?? stage;
const filteredAgents = agentConfig.filter(
(a) => a.stage === STAGE_TO_AGENT_STAGE[stage],
);
const activeAgentName =
agentInfo && (agentStatus === "running" || agentStatus === "pending")
? agentInfo.agent_name
: null;
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 16px",
borderBottom: "1px solid #333",
flexShrink: 0,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "2px",
minWidth: 0,
}}
>
<div
data-testid="detail-panel-title"
style={{
fontWeight: 600,
fontSize: "0.95em",
color: "#ececec",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{formatStoryTitle(storyId, name)}
</div>
{stage && (
<div
data-testid="detail-panel-stage"
style={{ fontSize: "0.75em", color: "#888" }}
>
{stageLabel}
</div>
)}
{filteredAgents.length > 0 && (
<div
data-testid="detail-panel-agent-assignment"
style={{
display: "flex",
alignItems: "center",
gap: "6px",
marginTop: "4px",
}}
>
<span style={{ fontSize: "0.75em", color: "#666" }}>Agent:</span>
<select
data-testid="agent-assignment-dropdown"
disabled={assigning}
value={activeAgentName ?? assignedAgent ?? ""}
onChange={(e) => onAgentAssign(e.target.value)}
style={{
background: "#1a1a1a",
border: "1px solid #444",
borderRadius: "4px",
color: "#ccc",
cursor: assigning ? "not-allowed" : "pointer",
fontSize: "0.75em",
padding: "2px 6px",
opacity: assigning ? 0.6 : 1,
}}
>
<option value=""> none </option>
{filteredAgents.map((a) => {
const isRunning =
agentInfo?.agent_name === a.name &&
agentStatus === "running";
const isPending =
agentInfo?.agent_name === a.name &&
agentStatus === "pending";
const statusLabel = isRunning
? " — running"
: isPending
? " — pending"
: " — idle";
const modelPart = a.model ? ` (${a.model})` : "";
return (
<option key={a.name} value={a.name}>
{a.name}
{modelPart}
{statusLabel}
</option>
);
})}
</select>
{assigning && (
<span style={{ fontSize: "0.7em", color: "#888" }}>
Assigning
</span>
)}
{assignError && (
<span
data-testid="agent-assignment-error"
style={{ fontSize: "0.7em", color: "#f85149" }}
>
{assignError}
</span>
)}
</div>
)}
{filteredAgents.length === 0 && assignedAgent ? (
<div
data-testid="detail-panel-assigned-agent"
style={{ fontSize: "0.75em", color: "#888" }}
>
Agent: {assignedAgent}
</div>
) : null}
</div>
<button
type="button"
data-testid="detail-panel-close"
onClick={onClose}
style={{
background: "none",
border: "1px solid #444",
borderRadius: "6px",
color: "#aaa",
cursor: "pointer",
padding: "4px 10px",
fontSize: "0.8em",
flexShrink: 0,
}}
>
Close
</button>
</div>
);
}