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

432 lines
14 KiB
TypeScript

import { useState } from "react";
import {
AlertTriangle,
Gauge,
ListChecks,
LoaderCircle,
RotateCcw,
X
} from "lucide-react";
import { Button } from "@tutti-os/ui-system";
import { TaskIcon } from "@tutti-os/ui-system/icons";
import {
TuttiModePlanPanel,
TuttiPlanIssuePanel,
type TuttiModePlanAssignmentCatalog,
type TuttiModePlanPanelLabels,
type TuttiModePlanPanelViewModel,
type TuttiModePlanTaskAssignmentDraft,
type TuttiModePlanTaskAssignmentDrafts,
type TuttiPlanIssuePanelLabels,
type TuttiPlanIssueSnapshot,
type TuttiPlanIssueTaskAction
} from "../../workspaceWorkflow";
import { cn } from "../../app/renderer/lib/utils";
import { AgentComposerDisclosureCard } from "./AgentComposerDisclosureCard";
import styles from "./AgentGUIChrome.styles";
import {
TuttiBudgetPopover,
type TuttiBudgetPopoverLabels
} from "./composer/TuttiBudgetPopover";
import { projectTuttiPreferencePreview } from "./composer/tuttiIntensityPreview";
export type TuttiWorkflowDockPhase =
| {
kind: "review";
effect: number;
speed: number;
preferencesDiverged: boolean;
panel: TuttiModePlanPanelViewModel;
submitting: boolean;
}
| {
kind: "materializing";
title: string;
}
| {
issue: TuttiPlanIssueSnapshot;
kind: "execution";
}
| {
kind: "error";
message: string;
retryable: boolean;
}
| {
kind: "reviewFailure";
message: string;
auditId?: string;
};
export interface TuttiWorkflowDockLabels {
cancel: string;
collapse: string;
errorTitle: string;
expand: string;
issueDone(done: string, total: string): string;
issueFailed(count: string): string;
issuePendingAcceptance(count: string): string;
issueRunning(count: string): string;
materializingHint: string;
materializingTitle: string;
retry: string;
switchToSelfReview: string;
switchingToSelfReview: string;
selfReviewEnabled: string;
selfReviewFailed: string;
reviewHint: string;
reviewHintReplan: string;
reviewTitle: string;
}
interface TuttiWorkflowDockDisclosureState {
expanded: boolean;
reviewPanelId: string | null;
}
function countIssueTasks(
issue: TuttiPlanIssueSnapshot,
status: string
): number {
return issue.tasks.filter((task) => task.status === status).length;
}
function issueSummary(
labels: TuttiWorkflowDockLabels,
issue: TuttiPlanIssueSnapshot
): string {
const running = countIssueTasks(issue, "running");
const pendingAcceptance = countIssueTasks(issue, "pending_acceptance");
const failed = countIssueTasks(issue, "failed");
const done = countIssueTasks(issue, "completed");
const parts: string[] = [];
if (running > 0) parts.push(labels.issueRunning(String(running)));
if (pendingAcceptance > 0) {
parts.push(labels.issuePendingAcceptance(String(pendingAcceptance)));
}
if (failed > 0) parts.push(labels.issueFailed(String(failed)));
parts.push(labels.issueDone(String(done), String(issue.tasks.length)));
return parts.join(" · ");
}
/**
* Single composer-anchored projection for the active Tutti workflow.
*
* The disclosure shell stays mounted while its phase moves from review to
* materialization and execution. Only one current panel is rendered, so the
* conversation timeline never competes with a second plan or task surface.
*/
export function TuttiWorkflowDock({
assignmentCatalog,
assignmentDrafts,
preferencePopoverLabels,
labels,
onAssignmentDraftChange,
onCancelReview,
onTaskAction,
onEffectChange,
onSpeedChange,
onOpenIssue,
onOpenTask,
onRetry,
onSwitchToSelfReview,
phase,
planPanelLabels,
planIssuePanelLabels
}: {
assignmentCatalog: TuttiModePlanAssignmentCatalog;
assignmentDrafts: TuttiModePlanTaskAssignmentDrafts;
preferencePopoverLabels: TuttiBudgetPopoverLabels;
labels: TuttiWorkflowDockLabels;
onAssignmentDraftChange(
taskId: string,
patch: TuttiModePlanTaskAssignmentDraft
): void;
onCancelReview(): void;
onTaskAction?: (
taskId: string,
action: TuttiPlanIssueTaskAction
) => Promise<void>;
onEffectChange(value: number): void;
onSpeedChange(value: number): void;
onOpenIssue?: () => void;
onOpenTask?: (taskId: string) => void | Promise<void>;
onRetry(): void;
onSwitchToSelfReview?: () => Promise<void>;
phase: TuttiWorkflowDockPhase;
planPanelLabels: TuttiModePlanPanelLabels;
planIssuePanelLabels: TuttiPlanIssuePanelLabels;
}): React.JSX.Element {
const review = phase.kind === "review" ? phase : null;
const execution = phase.kind === "execution" ? phase : null;
const failure = phase.kind === "error" ? phase : null;
const reviewFailure = phase.kind === "reviewFailure" ? phase : null;
const [selfReviewState, setSelfReviewState] = useState<
"idle" | "switching" | "enabled" | "failed"
>("idle");
const reviewPanelId = review?.panel.id ?? null;
const [disclosure, setDisclosure] =
useState<TuttiWorkflowDockDisclosureState>(() => ({
expanded: reviewPanelId !== null,
reviewPanelId
}));
// Echo preferences just dragged until canonical composer state catches up
// (review preference updates travel through an async daemon command),
// so the banner and the open popover never display diverging values. The
// echo clears during render (same adjustment pattern as the disclosure
// above) once the canonical value matches.
const [echoedEffect, setEchoedEffect] = useState<number | null>(null);
const [echoedSpeed, setEchoedSpeed] = useState<number | null>(null);
if (echoedEffect !== null && review?.effect === echoedEffect) {
setEchoedEffect(null);
}
if (echoedSpeed !== null && review?.speed === echoedSpeed) {
setEchoedSpeed(null);
}
const reviewDisplayEffect =
review !== null ? (echoedEffect ?? review.effect) : null;
const reviewDisplaySpeed =
review !== null ? (echoedSpeed ?? review.speed) : null;
const handleEffectChange = (value: number): void => {
setEchoedEffect(value);
onEffectChange(value);
};
const handleSpeedChange = (value: number): void => {
setEchoedSpeed(value);
onSpeedChange(value);
};
// A newly actionable checkpoint starts open once. Recording its stable panel
// identity prevents ordinary snapshot updates from overriding a user's
// explicit collapse, while phase handoffs retain the current disclosure.
if (reviewPanelId !== null && reviewPanelId !== disclosure.reviewPanelId) {
setDisclosure({ expanded: true, reviewPanelId });
}
const setExpanded = (expanded: boolean): void => {
setDisclosure((current) =>
current.expanded === expanded ? current : { ...current, expanded }
);
};
const title =
review !== null
? labels.reviewTitle
: phase.kind === "materializing"
? labels.materializingTitle
: execution !== null
? execution.issue.title
: labels.errorTitle;
const summary =
review !== null
? `${review.panel.title} · ${
review.preferencesDiverged
? labels.reviewHintReplan
: labels.reviewHint
}`
: phase.kind === "materializing"
? `${phase.title} · ${labels.materializingHint}`
: execution !== null
? issueSummary(labels, execution.issue)
: (failure?.message ??
(reviewFailure
? `${reviewFailure.message}${reviewFailure.auditId ? ` · ${reviewFailure.auditId}` : ""}`
: ""));
const icon =
review !== null ? (
<TaskIcon aria-hidden className="size-3.5" />
) : phase.kind === "materializing" ? (
<LoaderCircle aria-hidden className="size-3.5 animate-spin" />
) : execution !== null ? (
countIssueTasks(execution.issue, "running") > 0 ? (
<LoaderCircle aria-hidden className="size-3.5 animate-spin" />
) : (
<ListChecks aria-hidden className="size-3.5" />
)
) : (
<AlertTriangle aria-hidden className="size-3.5" />
);
const reviewTier =
reviewDisplayEffect !== null && reviewDisplaySpeed !== null
? projectTuttiPreferencePreview(reviewDisplayEffect, reviewDisplaySpeed)
.effectTier
: null;
const actions =
review !== null ? (
<>
<TuttiBudgetPopover
effect={reviewDisplayEffect ?? review.effect}
speed={reviewDisplaySpeed ?? review.speed}
labels={preferencePopoverLabels}
onEffectChange={handleEffectChange}
onSpeedChange={handleSpeedChange}
>
<button
type="button"
disabled={review.submitting}
title={preferencePopoverLabels.title}
aria-label={preferencePopoverLabels.title}
data-agent-tutti-tier={reviewTier ?? undefined}
data-testid="agent-gui-tutti-workflow-intensity"
className={cn(
"flex items-center gap-0.5 rounded-md px-1.5 py-0.5 text-[11px] transition-colors",
"data-[state=open]:bg-[color-mix(in_srgb,var(--tutti-purple)_12%,transparent)] data-[state=open]:text-[var(--tutti-purple)]"
)}
>
<Gauge aria-hidden className="size-3.5" />
{/*
All tier labels occupy the same grid cell so the chip width is
always the longest label's width; switching tiers while the
slider moves no longer resizes the chip or shifts the anchored
popover. Icon, label, and value share one container with a
uniform 4px gap.
*/}
<span className="grid">
{(
[
["cost", preferencePopoverLabels.previewCost],
["balance", preferencePopoverLabels.previewBalance],
["powerful", preferencePopoverLabels.previewPowerful]
] as const
).map(([tier, label]) => (
<span
key={tier}
aria-hidden={tier !== reviewTier}
className={
tier === reviewTier
? "[grid-area:1/1]"
: "invisible [grid-area:1/1]"
}
>
{label}
</span>
))}
</span>
{/*
Fixed-width value pair so dragging the sliders does not resize
the chip. Worst case is "100 · 100" (9ch), and whitespace-nowrap
guards against ch-unit rounding ever wrapping the pair.
*/}
<span className="inline-block w-[9ch] whitespace-nowrap text-left text-[11px] tabular-nums">
{reviewDisplayEffect ?? review.effect}
{" · "}
{reviewDisplaySpeed ?? review.speed}
</span>
</button>
</TuttiBudgetPopover>
<button
type="button"
disabled={review.submitting}
onClick={onCancelReview}
title={labels.cancel}
aria-label={labels.cancel}
data-testid="agent-gui-tutti-workflow-cancel"
className="flex size-5 items-center justify-center rounded-md transition-colors hover:bg-[var(--transparency-hover)]"
>
<X aria-hidden className="size-3.5" />
</button>
</>
) : reviewFailure ? (
<Button
type="button"
variant="secondary"
size="sm"
disabled={
!onSwitchToSelfReview ||
selfReviewState === "switching" ||
selfReviewState === "enabled"
}
onClick={() => {
if (!onSwitchToSelfReview) {
return;
}
setSelfReviewState("switching");
void onSwitchToSelfReview().then(
() => setSelfReviewState("enabled"),
() => setSelfReviewState("failed")
);
}}
>
{selfReviewState === "switching"
? labels.switchingToSelfReview
: selfReviewState === "enabled"
? labels.selfReviewEnabled
: labels.switchToSelfReview}
</Button>
) : phase.kind === "error" && phase.retryable ? (
<button
type="button"
onClick={onRetry}
title={labels.retry}
aria-label={labels.retry}
data-testid="agent-gui-tutti-workflow-retry"
>
<RotateCcw aria-hidden className="size-3.5" />
{labels.retry}
</button>
) : null;
return (
<AgentComposerDisclosureCard
actions={actions}
bannerClassName={styles.tuttiWorkflowBanner}
expanded={disclosure.expanded}
icon={icon}
labels={{ collapse: labels.collapse, expand: labels.expand }}
panelClassName={styles.tuttiWorkflowPanel}
onExpandedChange={setExpanded}
summary={summary}
testId="agent-gui-tutti-workflow-dock"
title={title}
>
{review !== null ? (
<TuttiModePlanPanel
assignmentCatalog={assignmentCatalog}
assignmentDrafts={assignmentDrafts}
embedded={true}
labels={planPanelLabels}
panel={review.panel}
submitting={review.submitting}
onAssignmentDraftChange={onAssignmentDraftChange}
/>
) : phase.kind === "materializing" ? (
<div
className="grid min-h-28 place-items-center gap-2 text-center text-sm text-muted-foreground"
role="status"
>
<LoaderCircle
aria-hidden
className="size-5 animate-spin text-[var(--tutti-purple)]"
/>
<span>{labels.materializingHint}</span>
</div>
) : execution !== null ? (
<TuttiPlanIssuePanel
embedded={true}
issue={execution.issue}
labels={planIssuePanelLabels}
onTaskAction={onTaskAction}
onOpenIssue={onOpenIssue}
onOpenTask={onOpenTask}
/>
) : (
<div
className="rounded-md border border-[color-mix(in_srgb,var(--state-danger)_45%,transparent)] px-4 py-3 text-sm text-muted-foreground"
role="alert"
>
<span>{failure?.message ?? reviewFailure?.message}</span>
{reviewFailure?.auditId ? (
<span className="mt-2 block text-xs">{reviewFailure.auditId}</span>
) : null}
{selfReviewState === "failed" ? (
<span className="mt-2 block text-xs">
{labels.selfReviewFailed}
</span>
) : null}
</div>
)}
</AgentComposerDisclosureCard>
);
}