Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

255 lines
7.6 KiB
TypeScript

import ReactMarkdown from "react-markdown";
import rehypeSanitize from "rehype-sanitize";
import remarkGfm from "remark-gfm";
import {
Badge,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
cn
} from "@tutti-os/ui-system";
import composerStyles from "../../agent-gui/agentGuiNode/AgentGUINode.styles";
import type {
TuttiModePlanPanelTaskViewModel,
TuttiModePlanPanelViewModel
} from "./tuttiModePlanPanelProjection";
import type { TuttiModePlanAssignmentCatalog } from "./useTuttiModePlanPanels";
import {
assignmentOptionLabel,
TuttiModePlanTaskAssignmentEditor,
permissionModeAssignmentTone
} from "./TuttiModePlanTaskAssignmentEditor";
import type {
TuttiModePlanTaskAssignmentDraft,
TuttiModePlanTaskAssignmentDrafts
} from "./tuttiModePlanTaskAssignments";
export interface TuttiModePlanPanelLabels {
tasks: string;
priority: string;
priorityHigh: string;
priorityMedium: string;
priorityLow: string;
agentTarget: string;
model: string;
permissionMode: string;
reasoningEffort: string;
parallelizable: string;
autoAccept: string;
notSpecified: string;
assignmentOptionsLoading: string;
}
/**
* Read-only review card for a pending Tutti mode plan. Decisions live in the
* composer (empty send accepts, typed feedback rejects, the review banner
* cancels), so the card carries no action buttons of its own; the only
* interactive piece is the per-task assignment row, whose drafts the host
* owns so an accept from the composer can include them.
*/
export function TuttiModePlanPanel({
assignmentCatalog,
assignmentDrafts,
embedded = false,
labels,
panel,
submitting,
onAssignmentDraftChange
}: {
assignmentCatalog?: TuttiModePlanAssignmentCatalog | null;
assignmentDrafts?: TuttiModePlanTaskAssignmentDrafts;
embedded?: boolean;
labels: TuttiModePlanPanelLabels;
panel: TuttiModePlanPanelViewModel;
submitting: boolean;
onAssignmentDraftChange?(
taskId: string,
patch: TuttiModePlanTaskAssignmentDraft
): void;
}): React.JSX.Element {
// Editing needs the loaded agent directory plus a host-owned draft store;
// before that (or without a host catalog at all) tasks stay read-only.
const editable =
assignmentCatalog?.agents != null &&
panel.actionable &&
onAssignmentDraftChange !== undefined;
return (
<Card
className={cn(
"w-full",
embedded
? "border-0 bg-transparent shadow-none"
: "mx-auto max-w-[860px]"
)}
data-testid="tutti-mode-plan-panel"
>
<CardHeader>
<CardTitle>{panel.title}</CardTitle>
<CardDescription>{panel.topicId}</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="prose prose-sm max-w-none text-foreground dark:prose-invert">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeSanitize]}
>
{panel.markdownBody}
</ReactMarkdown>
</div>
{panel.tasks.length > 0 ? (
<section className="grid gap-2">
<h4 className="text-xs font-medium text-foreground">
{labels.tasks}
</h4>
<ol className="grid gap-2">
{panel.tasks.map((task) => (
<li
key={task.id}
className="rounded-lg border border-[var(--line-2)] bg-[var(--background-board-card)] px-4 pt-3 pb-2 text-sm"
>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="font-medium text-foreground">
{task.ordinal}. {task.title}
</div>
<Badge variant="secondary">
{priorityLabel(labels, task.priority)}
</Badge>
</div>
{task.content ? (
<p className="mt-1 text-xs text-muted-foreground">
{task.content}
</p>
) : null}
{editable && assignmentCatalog ? (
<TuttiModePlanTaskAssignmentEditor
catalog={assignmentCatalog}
disabled={submitting}
draft={assignmentDrafts?.[task.id] ?? {}}
labels={labels}
task={task}
onEdit={(patch) =>
onAssignmentDraftChange?.(task.id, patch)
}
/>
) : (
<TaskAssignmentSummary
assignmentCatalog={assignmentCatalog}
labels={labels}
task={task}
/>
)}
</li>
))}
</ol>
</section>
) : null}
</CardContent>
</Card>
);
}
/**
* Read-only counterpart of the assignment editor row: the same composer-styled
* single line, showing only the assignments the plan actually specifies.
*/
function TaskAssignmentSummary({
assignmentCatalog,
labels,
task
}: {
assignmentCatalog?: TuttiModePlanAssignmentCatalog | null;
labels: TuttiModePlanPanelLabels;
task: TuttiModePlanPanelTaskViewModel;
}): React.JSX.Element | null {
const agentDetail = task.agentTargetId
? assignmentCatalog?.optionsByAgentId[task.agentTargetId]
: null;
const modelOptions = [
...(agentDetail?.models ?? []),
...(agentDetail?.modelPlans.flatMap((plan) => plan.models) ?? [])
];
const chips: {
label: string;
tone?: "accent" | "success" | "warning" | undefined;
value: string | null;
}[] = [
{
label: labels.agentTarget,
value: task.agentTargetId
? (assignmentCatalog?.agents?.find(
(agent) => agent.agentTargetId === task.agentTargetId
)?.label ?? task.agentTargetId)
: null
},
{
label: labels.model,
value: assignmentOptionLabel(modelOptions, task.model)
},
{
label: labels.permissionMode,
tone: permissionModeAssignmentTone(task.permissionModeId),
value: assignmentOptionLabel(
(agentDetail?.permissionModes ?? []).map((mode) => ({
label: mode.label,
value: mode.id
})),
task.permissionModeId
)
},
{
label: labels.reasoningEffort,
value: assignmentOptionLabel(
agentDetail?.reasoningEfforts ?? [],
task.reasoningEffort
)
},
{
label: labels.parallelizable,
tone: "accent" as const,
value: task.parallelizable ? labels.parallelizable : null
},
{
label: labels.autoAccept,
tone: "success" as const,
value: task.autoAccept ? labels.autoAccept : null
}
].filter((chip) => chip.value !== null);
if (chips.length === 0) {
return null;
}
return (
<div className="tutti-plan-task-assignment-row mt-3 flex flex-wrap items-center gap-3 border-t border-[var(--line-2)] pt-2">
{chips.map((chip) => (
<span
key={chip.label}
title={chip.label}
data-permission-tone={chip.tone}
className={cn(
"w-auto max-w-full",
composerStyles.composerMenuTrigger
)}
>
<span className="min-w-0 truncate">{chip.value}</span>
</span>
))}
</div>
);
}
function priorityLabel(
labels: TuttiModePlanPanelLabels,
priority: TuttiModePlanPanelViewModel["tasks"][number]["priority"]
): string {
switch (priority) {
case "high":
return labels.priorityHigh;
case "medium":
return labels.priorityMedium;
case "low":
return labels.priorityLow;
}
}