mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
235 lines
9.4 KiB
TypeScript
235 lines
9.4 KiB
TypeScript
import { useLayoutEffect, useState, type ReactNode } from "preact/compat";
|
|
|
|
import { activeAgentRunSummary } from "../display/agent";
|
|
import { executionState } from "../display/state";
|
|
import type {
|
|
AgentDisplayItem,
|
|
AgentRunSummary,
|
|
AgentRunSummaryAgent,
|
|
DisplayItem,
|
|
TaskProgressDisplayItem,
|
|
ToolDisplayItem,
|
|
} from "../display/types";
|
|
import { agentActivityMetaLabel, agentMessagePreview, agentRunSummaryLabel, taskStatusMarker } from "../display/labels";
|
|
import { activeTurnId, type ChatTurnLifecycleState } from "../chat-state";
|
|
import { createWorkMessageClassName } from "./work-message";
|
|
import { shortThreadId, truncate } from "../../../utils";
|
|
|
|
const AGENT_ROW_MESSAGE_PREVIEW_LIMIT = 120;
|
|
const AGENT_ACTIVITY_PROMPT_PREVIEW_LIMIT = 96;
|
|
|
|
type ReasoningDisplayItem = ToolDisplayItem & { kind: "reasoning" };
|
|
export type WorkItemDisplayItem = TaskProgressDisplayItem | AgentDisplayItem | ReasoningDisplayItem;
|
|
|
|
export interface WorkItemContext {
|
|
turnLifecycle: ChatTurnLifecycleState;
|
|
displayItems: readonly DisplayItem[];
|
|
openDetails: ReadonlySet<string>;
|
|
onDetailsToggle?: (key: string, open: boolean) => void;
|
|
}
|
|
|
|
export function workItemsActiveTurnId(context: Pick<WorkItemContext, "turnLifecycle">): string | null {
|
|
return activeTurnId(context);
|
|
}
|
|
|
|
export function activeAgentRunSummaryBlock(context: WorkItemContext): AgentRunSummary | null {
|
|
return activeAgentRunSummary(context.displayItems, workItemsActiveTurnId(context));
|
|
}
|
|
|
|
export function agentRunSummaryNode(summary: AgentRunSummary): ReactNode {
|
|
return <AgentRunSummaryItem summary={summary} />;
|
|
}
|
|
|
|
export function workItemNode(item: WorkItemDisplayItem, context: WorkItemContext): ReactNode {
|
|
if (item.kind === "taskProgress") return <TaskProgressItem item={item} />;
|
|
if (item.kind === "agent") return <AgentItem item={item} context={context} />;
|
|
return <ReasoningItem item={item} context={context} />;
|
|
}
|
|
|
|
function AgentRunSummaryItem({ summary }: { summary: AgentRunSummary }): ReactNode {
|
|
return (
|
|
<WorkMessage label="agents" className="codex-panel__agent-summary" state={summary.failed > 0 ? "failed" : "running"}>
|
|
<div className="codex-panel__tool-summary">{agentRunSummaryLabel(summary)}</div>
|
|
<AgentSummaryRows summary={summary} />
|
|
</WorkMessage>
|
|
);
|
|
}
|
|
|
|
function TaskProgressItem({ item }: { item: TaskProgressDisplayItem }): ReactNode {
|
|
return (
|
|
<WorkMessage label="tasks" className="codex-panel__task-progress" state={executionState(item)}>
|
|
{item.explanation ? <div className="codex-panel__tool-summary">{item.explanation}</div> : null}
|
|
{item.steps.length === 0 ? (
|
|
<div className="codex-panel__tool-summary">Plan updated</div>
|
|
) : (
|
|
<ul className="codex-panel__task-list">
|
|
{item.steps.map((step) => (
|
|
<li key={`${step.status}\n${step.step}`} className={`codex-panel__task-step codex-panel__task-step--${step.status}`}>
|
|
<span className="codex-panel__task-marker">{taskStatusMarker(step.status)}</span>
|
|
<span className="codex-panel__task-text">{step.step}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</WorkMessage>
|
|
);
|
|
}
|
|
|
|
function AgentItem({ item, context }: { item: AgentDisplayItem; context: WorkItemContext }): ReactNode {
|
|
const detailsKey = `${item.id}:agent-details`;
|
|
const [detailsOpen, setDetailsOpen] = useState(context.openDetails.has(detailsKey));
|
|
useLayoutEffect(() => {
|
|
setDetailsOpen(context.openDetails.has(detailsKey));
|
|
}, [context.openDetails, detailsKey]);
|
|
return (
|
|
<WorkMessage label="agent" className={`codex-panel__agent-activity${detailsOpen ? " is-open" : ""}`} state={executionState(item)}>
|
|
<div className="codex-panel__tool-summary codex-panel__agent-activity-summary">{agentSummaryText(item)}</div>
|
|
<details
|
|
className="codex-panel__output codex-panel__agent-details"
|
|
open={detailsOpen}
|
|
onToggle={(event) => {
|
|
const nextOpen = event.currentTarget.open;
|
|
setDetailsOpen(nextOpen);
|
|
context.onDetailsToggle?.(detailsKey, nextOpen);
|
|
}}
|
|
>
|
|
<summary>Details</summary>
|
|
<dl className="codex-panel__meta-grid">
|
|
<MetaPair name="tool" value={agentActivityMetaLabel(item.tool)} />
|
|
<MetaPair name="status" value={item.status} />
|
|
<MetaPair name="sender" value={item.senderThreadId} />
|
|
{item.receiverThreadIds.length > 0 ? <MetaPair name="target" value={item.receiverThreadIds.join(", ")} /> : null}
|
|
{item.model ? <MetaPair name="model" value={item.model} /> : null}
|
|
{item.reasoningEffort ? <MetaPair name="effort" value={item.reasoningEffort} /> : null}
|
|
</dl>
|
|
{item.prompt ? (
|
|
<section className="codex-panel__agent-detail-section">
|
|
<div className="codex-panel__output-title">Prompt</div>
|
|
<pre>{item.prompt}</pre>
|
|
</section>
|
|
) : null}
|
|
{item.agents.length > 0 ? (
|
|
<ul className="codex-panel__agent-list">
|
|
{item.agents.map((agent) => (
|
|
<li key={agent.threadId} className="codex-panel__agent-row">
|
|
<span className="codex-panel__agent-thread">{shortThreadId(agent.threadId)}</span>
|
|
<span className="codex-panel__agent-status">{agentStatusLabel(agent.status, agent.message)}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : null}
|
|
{item.agents.map((agent) =>
|
|
agent.message && isLongAgentMessage(agent.message) ? (
|
|
<section key={agent.threadId} className="codex-panel__agent-detail-section">
|
|
<div className="codex-panel__output-title">Agent output {shortThreadId(agent.threadId)}</div>
|
|
<pre>{agent.message}</pre>
|
|
</section>
|
|
) : null,
|
|
)}
|
|
</details>
|
|
</WorkMessage>
|
|
);
|
|
}
|
|
|
|
function ReasoningItem({ item, context }: { item: ReasoningDisplayItem; context: WorkItemContext }): ReactNode {
|
|
const active = isReasoningActive(item, context);
|
|
return (
|
|
<div className={`codex-panel__reasoning${active ? " is-active" : ""}`}>
|
|
<div className="codex-panel__reasoning-role">{active ? "reasoning" : "thought"}</div>
|
|
<div className="codex-panel__reasoning-content">
|
|
<span>{item.text || (active ? "Reasoning" : "Thought")}</span>
|
|
{active ? (
|
|
<span className="codex-panel__reasoning-dots">
|
|
<span>.</span>
|
|
<span>.</span>
|
|
<span>.</span>
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function WorkMessage({
|
|
label,
|
|
className,
|
|
state,
|
|
children,
|
|
}: {
|
|
label: string;
|
|
className: string;
|
|
state: ReturnType<typeof executionState>;
|
|
children: ReactNode;
|
|
}): ReactNode {
|
|
const classes = [createWorkMessageClassName(className), state ? `codex-panel__execution codex-panel__execution--${state}` : ""]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
return (
|
|
<div className={classes}>
|
|
<div className="codex-panel__message-role">{label}</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MetaPair({ name, value }: { name: string; value: string }): ReactNode {
|
|
return (
|
|
<>
|
|
<dt>{name}</dt>
|
|
<dd>{value}</dd>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function AgentSummaryRows({ summary }: { summary: AgentRunSummary }): ReactNode {
|
|
if (summary.agents.length === 0 && summary.additionalAgents === 0) return null;
|
|
return (
|
|
<ul className="codex-panel__agent-list codex-panel__agent-list--summary">
|
|
{summary.agents.map((agent) => (
|
|
<li key={agent.threadId} className="codex-panel__agent-row">
|
|
<span className="codex-panel__agent-thread">{shortThreadId(agent.threadId)}</span>
|
|
<span className="codex-panel__agent-status">{agentSummaryStatusLabel(agent)}</span>
|
|
</li>
|
|
))}
|
|
{summary.additionalAgents > 0 ? (
|
|
<li className="codex-panel__agent-row codex-panel__agent-row--more">
|
|
<span className="codex-panel__agent-thread" />
|
|
<span className="codex-panel__agent-status">+{String(summary.additionalAgents)} more</span>
|
|
</li>
|
|
) : null}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
function agentSummaryText(item: AgentDisplayItem): string {
|
|
const target = item.receiverThreadIds.length === 0 ? "" : ` ${item.receiverThreadIds.map(shortThreadId).join(", ")}`;
|
|
const promptPreview = agentPromptPreview(item.prompt);
|
|
return `${agentActivityMetaLabel(item.tool)}${target}${promptPreview ? `: ${promptPreview}` : ""} (${item.status})`;
|
|
}
|
|
|
|
function agentPromptPreview(prompt: string | null): string | null {
|
|
if (!prompt) return null;
|
|
const normalized = prompt.trim().replace(/\s+/g, " ");
|
|
return normalized ? truncate(normalized, AGENT_ACTIVITY_PROMPT_PREVIEW_LIMIT) : null;
|
|
}
|
|
|
|
function agentStatusLabel(status: string, message: string | null): string {
|
|
const preview = agentMessagePreview(message, AGENT_ROW_MESSAGE_PREVIEW_LIMIT);
|
|
return preview ? `${status}: ${preview}` : status;
|
|
}
|
|
|
|
function agentSummaryStatusLabel(agent: AgentRunSummaryAgent): string {
|
|
return agent.messagePreview ? `${agent.status}: ${agent.messagePreview}` : agent.status;
|
|
}
|
|
|
|
function isLongAgentMessage(message: string): boolean {
|
|
return message.length > AGENT_ROW_MESSAGE_PREVIEW_LIMIT || message.includes("\n");
|
|
}
|
|
|
|
function isReasoningActive(item: ReasoningDisplayItem, context: WorkItemContext): boolean {
|
|
const activeTurn = workItemsActiveTurnId(context);
|
|
if (!activeTurn || item.turnId !== activeTurn) return false;
|
|
if (executionState(item) === "completed") return false;
|
|
const latestActiveTurnItem = [...context.displayItems].reverse().find((candidate) => candidate.turnId === activeTurn);
|
|
return latestActiveTurnItem?.id === item.id;
|
|
}
|