185 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|