源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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

This commit is contained in:
agent-radar
2026-09-01 20:17:27 +08:00
commit 2ed3415fb7
8018 changed files with 1936476 additions and 0 deletions
@@ -0,0 +1,2 @@
export * from "./workspaceWorkflowRuntime";
export * from "./tuttiModePlan";
@@ -0,0 +1,258 @@
import "@testing-library/jest-dom/vitest";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import {
TuttiModePlanPanel,
type TuttiModePlanPanelLabels
} from "./TuttiModePlanPanel";
import type { TuttiModePlanPanelViewModel } from "./tuttiModePlanPanelProjection";
import type { TuttiModePlanAssignmentCatalog } from "./useTuttiModePlanPanels";
const labels: TuttiModePlanPanelLabels = {
tasks: "Tasks",
priority: "Priority",
priorityHigh: "High",
priorityMedium: "Medium",
priorityLow: "Low",
agentTarget: "Agent",
model: "Model",
permissionMode: "Permission mode",
reasoningEffort: "Reasoning effort",
parallelizable: "Parallel",
autoAccept: "Auto-accept",
notSpecified: "Not specified",
assignmentOptionsLoading: "Loading options..."
};
const panel: TuttiModePlanPanelViewModel = {
id: "workflow-1:checkpoint-1",
workflowId: "workflow-1",
workspaceId: "workspace-1",
sourceSessionId: "session-1",
sourceTurnId: "turn-1",
sourceToolCallId: "tool-call-1",
reviewKind: "task_review",
state: "pending",
actionable: true,
title: "Ship the durable workflow",
topicId: "topic-1",
markdownBody: "## Goal\n\nShip safely.",
revision: {
id: "revision-1",
sequence: 1,
schemaVersion: "tutti-mode-plan/v1",
documentPath: `tutti-mode-plans/workflow-1/revisions/${"a".repeat(64)}.md`,
sha256: "a".repeat(64),
producedByTurnId: "turn-1",
createdAtUnixMs: 100
},
checkpoint: {
id: "checkpoint-1",
status: "pending",
decidedBy: null,
decisionReason: null,
decidedAtUnixMs: null,
createdAtUnixMs: 110,
updatedAtUnixMs: 110
},
execution: {
mode: "sequential",
reasoningIntensity: 70,
orchestrationIntensity: 60
},
budget: {
mode: "fixed",
tokenLimit: 12_000,
quotaWaterlinePercent: 15
},
tasks: [
{
ordinal: 1,
id: "implement",
title: "Implement",
content: "Build the workflow",
priority: "high",
agentTargetId: "codex-agent",
modelPlanId: "model-plan-pro",
model: "gpt-5.6-sol",
permissionModeId: "acceptEdits",
reasoningEffort: "high",
executionDirectory: "/workspace/implement",
dependsOn: ["foundation", "contract-tests"],
parallelizable: true,
autoAccept: true
}
]
};
function assignmentCatalog(
overrides: Partial<TuttiModePlanAssignmentCatalog> = {}
): TuttiModePlanAssignmentCatalog {
return {
agents: [
{ agentTargetId: "codex-agent", label: "Codex" },
{ agentTargetId: "claude-agent", label: "Claude Code" }
],
optionsByAgentId: {
"codex-agent": {
models: [
{ value: "gpt-5.6-sol", label: "GPT-5.6 Sol" },
{
value: "composer-2.5[fast=true]",
label: "composer-2.5"
}
],
modelPlans: [
{
modelPlanId: "model-plan-pro",
label: "Pro plan",
models: [{ value: "gpt-5.6-sol", label: "GPT-5.6 Sol" }]
}
],
permissionModes: [{ id: "acceptEdits", label: "Accept edits" }],
reasoningEfforts: [
{ value: "low", label: "Low" },
{ value: "high", label: "High" }
]
}
},
loadAgentOptions: vi.fn(),
...overrides
};
}
describe("TuttiModePlanPanel", () => {
it("renders the plan without duplicating workflow phase badges", () => {
render(
<TuttiModePlanPanel labels={labels} panel={panel} submitting={false} />
);
expect(screen.getByText("Ship the durable workflow")).toBeInTheDocument();
expect(screen.queryByText("Tutti mode plan")).not.toBeInTheDocument();
expect(screen.queryByText("Plan review")).not.toBeInTheDocument();
expect(screen.queryByText("Needs review")).not.toBeInTheDocument();
});
it("hides plan internals: execution/budget cards, task metadata, decisions", () => {
render(
<TuttiModePlanPanel labels={labels} panel={panel} submitting={false} />
);
// Execution / budget tuning happens by re-planning, not on the card.
expect(screen.queryByText("Execution")).not.toBeInTheDocument();
expect(screen.queryByText("Budget")).not.toBeInTheDocument();
expect(screen.queryByText("Sequential")).not.toBeInTheDocument();
expect(screen.queryByText("Quota waterline")).not.toBeInTheDocument();
expect(screen.queryByText("15%")).not.toBeInTheDocument();
// Task ID / execution directory / dependencies stay out of the card.
expect(screen.queryByText("/workspace/implement")).not.toBeInTheDocument();
expect(screen.queryByText("foundation")).not.toBeInTheDocument();
// Decisions live in the composer now — no footer buttons.
expect(
screen.queryByRole("button", { name: "Accept" })
).not.toBeInTheDocument();
});
it("shows specified assignments read-only without an assignment catalog", () => {
render(
<TuttiModePlanPanel labels={labels} panel={panel} submitting={false} />
);
expect(screen.getByText("codex-agent")).toBeInTheDocument();
expect(screen.getByText("gpt-5.6-sol")).toBeInTheDocument();
expect(screen.getByText("acceptEdits")).toBeInTheDocument();
expect(screen.getByText("high")).toBeInTheDocument();
expect(screen.getByText("Parallel")).toBeInTheDocument();
// Model plans never surface on tasks — plans enter via agent config.
expect(screen.queryByText("model-plan-pro")).not.toBeInTheDocument();
});
it("renders per-task assignment selectors without a model plan select", () => {
render(
<TuttiModePlanPanel
assignmentCatalog={assignmentCatalog()}
assignmentDrafts={{}}
labels={labels}
panel={panel}
submitting={false}
onAssignmentDraftChange={vi.fn()}
/>
);
expect(
screen.getByTestId("tutti-plan-task-assignment-implement")
).toBeInTheDocument();
expect(screen.getByLabelText("Agent")).toBeInTheDocument();
expect(screen.getByLabelText("Model")).toBeInTheDocument();
expect(screen.getByLabelText("Permission mode")).toBeInTheDocument();
expect(screen.getByLabelText("Reasoning effort")).toBeInTheDocument();
expect(screen.queryByLabelText("Model plan")).not.toBeInTheDocument();
// The parallel opt-in renders as a checked switch seeded from the plan.
const parallelSwitch = screen.getByTestId(
"tutti-plan-task-parallel-toggle-implement"
);
expect(parallelSwitch).toHaveAttribute("role", "switch");
expect(parallelSwitch).toHaveAttribute("aria-checked", "true");
});
it("renders the canonical model label instead of the provider-native value", () => {
render(
<TuttiModePlanPanel
assignmentCatalog={assignmentCatalog()}
assignmentDrafts={{}}
labels={labels}
panel={{
...panel,
tasks: [
{
...panel.tasks[0]!,
model: "composer-2.5[fast=true]"
}
]
}}
submitting={false}
onAssignmentDraftChange={vi.fn()}
/>
);
expect(screen.getByLabelText("Model")).toHaveTextContent("composer-2.5");
expect(screen.getByLabelText("Model")).not.toHaveTextContent("[fast=true]");
});
it("stays read-only without a host-owned draft store", () => {
render(
<TuttiModePlanPanel
assignmentCatalog={assignmentCatalog()}
labels={labels}
panel={panel}
submitting={false}
/>
);
expect(
screen.queryByTestId("tutti-plan-task-assignment-implement")
).not.toBeInTheDocument();
});
it("stays read-only for non-actionable panels even with a catalog", () => {
render(
<TuttiModePlanPanel
assignmentCatalog={assignmentCatalog()}
assignmentDrafts={{}}
labels={labels}
panel={{ ...panel, state: "accepted", actionable: false }}
submitting={false}
onAssignmentDraftChange={vi.fn()}
/>
);
expect(
screen.queryByTestId("tutti-plan-task-assignment-implement")
).not.toBeInTheDocument();
expect(screen.getByTitle("Model")).toHaveTextContent("GPT-5.6 Sol");
expect(screen.getByTitle("Permission mode")).toHaveTextContent(
"Accept edits"
);
expect(screen.getByTitle("Reasoning effort")).toHaveTextContent("High");
});
});
@@ -0,0 +1,254 @@
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;
}
}
@@ -0,0 +1,379 @@
import { CheckCheck, Split } from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
Switch,
cn
} from "@tutti-os/ui-system";
import composerStyles from "../../agent-gui/agentGuiNode/AgentGUINode.styles";
import type { TuttiModePlanAssignmentCatalog } from "./useTuttiModePlanPanels";
import type { TuttiModePlanPanelTaskViewModel } from "./tuttiModePlanPanelProjection";
import {
effectiveTaskAssignmentValue,
effectiveTaskFlag,
type TuttiModePlanTaskAssignmentDraft
} from "./tuttiModePlanTaskAssignments";
export interface TuttiModePlanTaskAssignmentEditorLabels {
agentTarget: string;
model: string;
permissionMode: string;
reasoningEffort: string;
parallelizable: string;
autoAccept: string;
notSpecified: string;
assignmentOptionsLoading: string;
}
const CLEAR_VALUE = "__tutti_plan_assignment_clear__";
function toSelectValue(value: string): string {
return value === "" ? CLEAR_VALUE : value;
}
function fromSelectValue(value: string): string {
return value === CLEAR_VALUE ? "" : value;
}
/**
* Mirrors the composer permission-mode trigger tones so the plan panel's
* permission controls pick up the same accent/success/warning colors.
*/
export function permissionModeAssignmentTone(
value: string | null | undefined
): "accent" | "success" | "warning" | undefined {
switch (value?.trim().toLowerCase().replace(/\s+/g, "-") || undefined) {
case "read-only":
case "readonly":
case "ask-for-approval":
return "success";
case "auto":
case "default":
case "accept-edits":
case "acceptedits":
return "accent";
case "full-access":
case "bypasspermissions":
return "warning";
default:
return undefined;
}
}
export function assignmentOptionLabel(
options: readonly { label: string; value: string }[],
value: string | null | undefined
): string | null {
const normalizedValue = value?.trim() ?? "";
if (!normalizedValue) return null;
return (
options.find((option) => option.value === normalizedValue)?.label ??
normalizedValue
);
}
/**
* Per-task assignment selectors for the single review checkpoint, rendered as
* one composer-styled row. Option catalogs are agent-scoped and supplied by
* the host; the editor only chooses among them and reports draft edits upward.
*
* Model plans are deliberately not exposed here: a plan enters orchestration
* by configuring an agent target bound to it, so the row offers only agent /
* model / permission mode / reasoning effort. An explicit model choice clears
* any document-stamped plan pin so the two can never contradict.
*/
export function TuttiModePlanTaskAssignmentEditor({
catalog,
disabled,
draft,
labels,
onEdit,
task
}: {
catalog: TuttiModePlanAssignmentCatalog;
disabled: boolean;
draft: TuttiModePlanTaskAssignmentDraft;
labels: TuttiModePlanTaskAssignmentEditorLabels;
onEdit(patch: TuttiModePlanTaskAssignmentDraft): void;
task: TuttiModePlanPanelTaskViewModel;
}): React.JSX.Element {
const agentValue = effectiveTaskAssignmentValue(
draft.agentTargetId,
task.agentTargetId
);
const modelValue = effectiveTaskAssignmentValue(draft.model, task.model);
const permissionValue = effectiveTaskAssignmentValue(
draft.permissionModeId,
task.permissionModeId
);
const effortValue = effectiveTaskAssignmentValue(
draft.reasoningEffort,
task.reasoningEffort
);
const parallelizable = effectiveTaskFlag(
draft.parallelizable,
task.parallelizable
);
const autoAccept = effectiveTaskFlag(draft.autoAccept, task.autoAccept);
// Options for document-referenced agents are preloaded by the panels hook
// alongside the snapshot refresh; user-driven agent changes trigger the
// (deduplicated) load from the change handler, so no component effect is
// needed here.
const agentDetail = agentValue
? (catalog.optionsByAgentId[agentValue] ?? null)
: null;
const modelOptions = agentDetail?.models ?? [];
const detailPending = Boolean(agentValue) && !agentDetail;
const agentLabel = agentValue
? ((catalog.agents ?? []).find(
(agent) => agent.agentTargetId === agentValue
)?.label ?? agentValue)
: 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"
data-testid={`tutti-plan-task-assignment-${task.id}`}
>
<Select
disabled={disabled}
value={toSelectValue(agentValue)}
onValueChange={(value) => {
const nextAgent = fromSelectValue(value);
if (nextAgent) catalog.loadAgentOptions(nextAgent);
onEdit({ agentTargetId: nextAgent });
}}
>
<AssignmentSelectTrigger
fieldLabel={labels.agentTarget}
value={agentLabel}
/>
<AssignmentSelectContent>
<AssignmentSelectItem value={CLEAR_VALUE}>
{labels.notSpecified}
</AssignmentSelectItem>
{(catalog.agents ?? []).map((agent) => (
<AssignmentSelectItem
key={agent.agentTargetId}
value={agent.agentTargetId}
>
{agent.label}
</AssignmentSelectItem>
))}
{agentValue &&
!(catalog.agents ?? []).some(
(agent) => agent.agentTargetId === agentValue
) ? (
<AssignmentSelectItem value={agentValue}>
{agentValue}
</AssignmentSelectItem>
) : null}
</AssignmentSelectContent>
</Select>
<AssignmentValueSelect
disabled={disabled || !agentValue || detailPending}
fieldLabel={labels.model}
notSpecifiedLabel={labels.notSpecified}
options={modelOptions}
pending={detailPending}
pendingLabel={labels.assignmentOptionsLoading}
value={modelValue}
onChange={(value) => onEdit({ model: value, modelPlanId: "" })}
/>
<AssignmentValueSelect
disabled={disabled || !agentValue || detailPending}
fieldLabel={labels.permissionMode}
notSpecifiedLabel={labels.notSpecified}
options={(agentDetail?.permissionModes ?? []).map((mode) => ({
label: mode.label,
value: mode.id
}))}
pending={detailPending}
pendingLabel={labels.assignmentOptionsLoading}
tone={permissionModeAssignmentTone(permissionValue)}
value={permissionValue}
onChange={(value) => onEdit({ permissionModeId: value })}
/>
<AssignmentValueSelect
disabled={disabled || !agentValue || detailPending}
fieldLabel={labels.reasoningEffort}
notSpecifiedLabel={labels.notSpecified}
options={agentDetail?.reasoningEfforts ?? []}
pending={detailPending}
pendingLabel={labels.assignmentOptionsLoading}
value={effortValue}
onChange={(value) => onEdit({ reasoningEffort: value })}
/>
<label
className={cn(
"flex min-h-[28px] w-auto shrink-0 items-center gap-1.5 text-[13px]",
disabled
? "cursor-not-allowed text-[var(--agent-gui-text-tertiary)]"
: "cursor-pointer text-[var(--agent-gui-text-secondary)]"
)}
htmlFor={`tutti-plan-task-parallel-${task.id}`}
title={labels.parallelizable}
>
<Split aria-hidden className="size-3.5 shrink-0" />
<span className="min-w-0 truncate">{labels.parallelizable}</span>
<Switch
id={`tutti-plan-task-parallel-${task.id}`}
size="sm"
checked={parallelizable}
disabled={disabled}
aria-label={labels.parallelizable}
data-testid={`tutti-plan-task-parallel-toggle-${task.id}`}
onCheckedChange={(checked) => onEdit({ parallelizable: checked })}
/>
</label>
<label
className={cn(
"flex min-h-[28px] w-auto shrink-0 items-center gap-1.5 text-[13px]",
disabled
? "cursor-not-allowed text-[var(--agent-gui-text-tertiary)]"
: "cursor-pointer text-[var(--agent-gui-text-secondary)]"
)}
htmlFor={`tutti-plan-task-auto-accept-${task.id}`}
title={labels.autoAccept}
>
<CheckCheck aria-hidden className="size-3.5 shrink-0" />
<span className="min-w-0 truncate">{labels.autoAccept}</span>
<Switch
id={`tutti-plan-task-auto-accept-${task.id}`}
size="sm"
checked={autoAccept}
disabled={disabled}
aria-label={labels.autoAccept}
data-testid={`tutti-plan-task-auto-accept-toggle-${task.id}`}
onCheckedChange={(checked) => onEdit({ autoAccept: checked })}
/>
</label>
</div>
);
}
function AssignmentSelectTrigger({
fieldLabel,
pending = false,
pendingLabel,
tone,
value
}: {
fieldLabel: string;
pending?: boolean;
pendingLabel?: string;
tone?: "accent" | "success" | "warning" | undefined;
value: string | null;
}): React.JSX.Element {
const placeholder = pending && pendingLabel ? pendingLabel : fieldLabel;
return (
<SelectTrigger
aria-label={fieldLabel}
title={fieldLabel}
data-permission-tone={value !== null ? tone : undefined}
className={cn(
"w-auto max-w-full",
composerStyles.composerMenuTrigger,
pending && "animate-pulse"
)}
>
<span className="flex min-w-0 flex-1 items-center">
<span
className={cn(
"truncate",
value === null && "text-[var(--agent-gui-text-secondary)]"
)}
>
{value ?? placeholder}
</span>
</span>
</SelectTrigger>
);
}
function AssignmentSelectContent({
children
}: {
children: React.ReactNode;
}): React.JSX.Element {
return (
<SelectContent
className={cn(
"nodrag",
composerStyles.composerMenuContent,
"min-w-[190px]"
)}
>
{children}
</SelectContent>
);
}
function AssignmentSelectItem({
children,
value
}: {
children: React.ReactNode;
value: string;
}): React.JSX.Element {
return (
<SelectItem className={composerStyles.composerMenuItem} value={value}>
{children}
</SelectItem>
);
}
function AssignmentValueSelect({
disabled,
fieldLabel,
notSpecifiedLabel,
onChange,
options,
pending,
pendingLabel,
tone,
value
}: {
disabled: boolean;
fieldLabel: string;
notSpecifiedLabel: string;
onChange(value: string): void;
options: readonly { label: string; value: string }[];
pending: boolean;
pendingLabel: string;
tone?: "accent" | "success" | "warning" | undefined;
value: string;
}): React.JSX.Element {
const known = options.some((option) => option.value === value);
const selectedLabel = assignmentOptionLabel(options, value);
return (
<Select
disabled={disabled}
value={toSelectValue(value)}
onValueChange={(next) => onChange(fromSelectValue(next))}
>
<AssignmentSelectTrigger
fieldLabel={fieldLabel}
pending={pending}
pendingLabel={pendingLabel}
tone={tone}
value={selectedLabel}
/>
<AssignmentSelectContent>
<AssignmentSelectItem value={CLEAR_VALUE}>
{notSpecifiedLabel}
</AssignmentSelectItem>
{options.map((option) => (
<AssignmentSelectItem key={option.value} value={option.value}>
{option.label}
</AssignmentSelectItem>
))}
{value && !known ? (
<AssignmentSelectItem value={value}>{value}</AssignmentSelectItem>
) : null}
</AssignmentSelectContent>
</Select>
);
}
@@ -0,0 +1,371 @@
import "@testing-library/jest-dom/vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import {
TuttiPlanIssuePanel,
groupTuttiPlanIssueTasksIntoStages,
type TuttiPlanIssuePanelLabels
} from "./TuttiPlanIssuePanel";
import type { TuttiPlanIssueSnapshot } from "../workspaceWorkflowRuntime";
const labels: TuttiPlanIssuePanelLabels = {
openIssue: "Open Issue",
listView: "List",
boardView: "Board",
parallelizable: "Parallel",
autoAccept: "Auto-accept",
accept: "Accept",
rework: "Rework",
dependencies: "Depends",
stageParallel: (index, count) => `Stage ${index} · parallel ×${count}`,
stageSequential: (index) => `Stage ${index} · sequential`,
summary: (done, total, running) =>
`${done}/${total} done · ${running} running`,
statusNotStarted: "Todo",
statusRunning: "Running",
statusPendingAcceptance: "In review",
statusCompleted: "Done",
statusFailed: "Failed",
statusCanceled: "Canceled"
};
const issue: TuttiPlanIssueSnapshot = {
workflowId: "wf-1",
sourceTurnId: "turn-1",
issueId: "tutti-mode-plan-wf-1",
topicId: "default",
title: "Neon chase MVP",
dispatchPaused: false,
tasks: [
{
taskId: "p1",
title: "Build the stage",
content: "Foundations",
status: "completed",
sortIndex: 1,
parallelizable: true,
autoAccept: false,
dependencyTaskIds: []
},
{
taskId: "p2",
title: "Core gameplay",
content: "WASD",
status: "running",
sortIndex: 2,
parallelizable: true,
autoAccept: true,
dependencyTaskIds: []
},
{
taskId: "s3",
title: "Polish and ship",
content: "QA",
status: "not_started",
sortIndex: 3,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: ["p1", "p2"]
}
]
};
describe("groupTuttiPlanIssueTasksIntoStages", () => {
it("splits chained parallelizable tasks into separate stages", () => {
// A parallelizable task that depends on a member of the running stage can
// never actually run alongside it, so the display must not pretend it can.
const chained: TuttiPlanIssueSnapshot["tasks"] = [
{
taskId: "c1",
title: "First",
content: "",
status: "not_started",
sortIndex: 1,
parallelizable: true,
autoAccept: false,
dependencyTaskIds: []
},
{
taskId: "c2",
title: "Second",
content: "",
status: "not_started",
sortIndex: 2,
parallelizable: true,
autoAccept: false,
dependencyTaskIds: ["c1"]
},
{
taskId: "c3",
title: "Third",
content: "",
status: "not_started",
sortIndex: 3,
parallelizable: true,
autoAccept: false,
dependencyTaskIds: []
}
];
const stages = groupTuttiPlanIssueTasksIntoStages(chained);
expect(
stages.map((stage) => stage.tasks.map((task) => task.taskId))
).toEqual([["c1"], ["c2", "c3"]]);
});
it("mirrors dispatcher semantics: parallel batch then exclusive stage", () => {
const stages = groupTuttiPlanIssueTasksIntoStages(issue.tasks);
expect(stages.map((stage) => stage.kind)).toEqual([
"parallel",
"sequential"
]);
expect(stages[0]?.tasks.map((task) => task.taskId)).toEqual(["p1", "p2"]);
expect(stages[1]?.tasks.map((task) => task.taskId)).toEqual(["s3"]);
});
});
describe("TuttiPlanIssuePanel", () => {
it("defaults to the board with status columns and structure chips", () => {
render(
<TuttiPlanIssuePanel
issue={issue}
labels={labels}
onOpenIssue={vi.fn()}
/>
);
expect(screen.getByText("Neon chase MVP")).toBeInTheDocument();
expect(screen.getByText("1/3 done · 1 running")).toBeInTheDocument();
expect(
screen.getByTestId("tutti-plan-issue-column-running")
).toHaveTextContent("Core gameplay");
expect(
screen.getByTestId("tutti-plan-issue-column-completed")
).toHaveTextContent("Build the stage");
// No failed/canceled tasks: those columns stay hidden.
expect(
screen.queryByTestId("tutti-plan-issue-column-failed")
).not.toBeInTheDocument();
expect(screen.getAllByText("Parallel").length).toBeGreaterThan(0);
});
it("switches to the stage-grouped list view", () => {
render(
<TuttiPlanIssuePanel
issue={issue}
labels={labels}
onOpenIssue={vi.fn()}
/>
);
fireEvent.click(screen.getByTestId("tutti-plan-issue-view-list"));
expect(
screen.getByTestId("tutti-plan-issue-stage-parallel")
).toHaveTextContent("Stage 1 · parallel ×2");
expect(
screen.getByTestId("tutti-plan-issue-stage-sequential")
).toHaveTextContent("Stage 2 · sequential");
expect(screen.getByText("Depends: p1, p2")).toBeInTheDocument();
});
it("jumps to the full issue surface", () => {
const onOpenIssue = vi.fn();
render(
<TuttiPlanIssuePanel
issue={issue}
labels={labels}
onOpenIssue={onOpenIssue}
/>
);
fireEvent.click(screen.getByTestId("tutti-plan-issue-open"));
expect(onOpenIssue).toHaveBeenCalledTimes(1);
});
it("does not expose a panel-local stop while runs are live", () => {
const liveTask = {
taskId: "live",
title: "Working",
content: "",
status: "running",
sortIndex: 1,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
};
const runningIssue: TuttiPlanIssueSnapshot = {
...issue,
tasks: [liveTask]
};
const { rerender } = render(
<TuttiPlanIssuePanel issue={runningIssue} labels={labels} />
);
expect(screen.queryByTestId("tutti-plan-issue-stop")).toBeNull();
rerender(
<TuttiPlanIssuePanel
issue={{
...runningIssue,
tasks: [{ ...liveTask, status: "canceled" }]
}}
labels={labels}
/>
);
expect(screen.queryByTestId("tutti-plan-issue-stop")).toBeNull();
});
it("jumps into the delegate conversation from a launched task card", () => {
const launchedIssue: TuttiPlanIssueSnapshot = {
...issue,
tasks: [
{
taskId: "live",
title: "Working",
content: "",
status: "running",
sortIndex: 1,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
},
{
taskId: "gate",
title: "Review me",
content: "",
status: "pending_acceptance",
sortIndex: 2,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
},
{
taskId: "later",
title: "Not launched",
content: "",
status: "not_started",
sortIndex: 3,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
}
]
};
const onOpenTask = vi.fn();
const onTaskAction = vi.fn().mockResolvedValue(undefined);
render(
<TuttiPlanIssuePanel
issue={launchedIssue}
labels={labels}
onOpenTask={onOpenTask}
onTaskAction={onTaskAction}
/>
);
fireEvent.click(screen.getByTestId("tutti-plan-issue-task-live"));
expect(onOpenTask).toHaveBeenCalledWith("live");
// A task that never launched has no conversation to jump into.
fireEvent.click(screen.getByTestId("tutti-plan-issue-task-later"));
expect(onOpenTask).toHaveBeenCalledTimes(1);
// Prompt actions must not also open the delegate task conversation.
fireEvent.click(screen.getByTestId("tutti-plan-issue-accept-gate"));
expect(onTaskAction).toHaveBeenCalledWith("gate", "accept");
expect(onOpenTask).toHaveBeenCalledTimes(1);
});
it("shows the auto-accept chip on flagged tasks", () => {
render(
<TuttiPlanIssuePanel
issue={issue}
labels={labels}
onOpenIssue={vi.fn()}
/>
);
expect(screen.getAllByText("Auto-accept").length).toBeGreaterThan(0);
});
it("requests source-Agent prompts for pending task actions", async () => {
const pendingIssue: TuttiPlanIssueSnapshot = {
...issue,
tasks: [
{
taskId: "gate",
title: "Review me",
content: "",
status: "pending_acceptance",
sortIndex: 1,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
}
]
};
const onTaskAction = vi.fn().mockResolvedValue(undefined);
render(
<TuttiPlanIssuePanel
issue={pendingIssue}
labels={labels}
onTaskAction={onTaskAction}
/>
);
fireEvent.click(screen.getByTestId("tutti-plan-issue-accept-gate"));
expect(onTaskAction).toHaveBeenCalledWith("gate", "accept");
// In-flight prompt preparation disables both buttons until it settles.
expect(screen.getByTestId("tutti-plan-issue-rework-gate")).toBeDisabled();
await waitFor(() =>
expect(screen.getByTestId("tutti-plan-issue-rework-gate")).toBeEnabled()
);
fireEvent.click(screen.getByTestId("tutti-plan-issue-rework-gate"));
expect(onTaskAction).toHaveBeenCalledWith("gate", "rework");
});
it("offers only a rework prompt action on a failed task", () => {
const failedIssue: TuttiPlanIssueSnapshot = {
...issue,
tasks: [
{
taskId: "boom",
title: "Broke",
content: "",
status: "failed",
sortIndex: 1,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
}
]
};
const onTaskAction = vi.fn().mockResolvedValue(undefined);
render(
<TuttiPlanIssuePanel
issue={failedIssue}
labels={labels}
onTaskAction={onTaskAction}
/>
);
expect(
screen.queryByTestId("tutti-plan-issue-accept-boom")
).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId("tutti-plan-issue-rework-boom"));
expect(onTaskAction).toHaveBeenCalledWith("boom", "rework");
});
it("keeps pending tasks display-only without a prompt callback", () => {
const pendingIssue: TuttiPlanIssueSnapshot = {
...issue,
tasks: [
{
taskId: "gate",
title: "Review me",
content: "",
status: "pending_acceptance",
sortIndex: 1,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
}
]
};
render(<TuttiPlanIssuePanel issue={pendingIssue} labels={labels} />);
expect(
screen.queryByTestId("tutti-plan-issue-accept-gate")
).not.toBeInTheDocument();
});
});
@@ -0,0 +1,577 @@
import { useState } from "react";
import { ExternalLink, ListChecks, RotateCcw } from "lucide-react";
import {
Badge,
Button,
Card,
CardContent,
CardHeader,
SegmentBar,
cn
} from "@tutti-os/ui-system";
import type {
TuttiPlanIssueSnapshot,
TuttiPlanIssueTaskSnapshot
} from "../workspaceWorkflowRuntime";
import type { TuttiModePlanTaskPromptAction } from "./tuttiModePlanPromptActions";
export interface TuttiPlanIssuePanelLabels {
openIssue: string;
listView: string;
boardView: string;
parallelizable: string;
autoAccept: string;
accept: string;
rework: string;
dependencies: string;
stageParallel: (index: string, count: string) => string;
stageSequential: (index: string) => string;
summary: (done: string, total: string, running: string) => string;
statusNotStarted: string;
statusRunning: string;
statusPendingAcceptance: string;
statusCompleted: string;
statusFailed: string;
statusCanceled: string;
}
type TuttiPlanIssueViewMode = "list" | "board";
const BOARD_STATUS_ORDER = [
"not_started",
"running",
"pending_acceptance",
"completed",
"failed",
"canceled"
] as const;
type BoardStatus = (typeof BOARD_STATUS_ORDER)[number];
function boardStatusOf(task: TuttiPlanIssueTaskSnapshot): BoardStatus {
return (BOARD_STATUS_ORDER as readonly string[]).includes(task.status)
? (task.status as BoardStatus)
: "not_started";
}
function statusLabel(
labels: TuttiPlanIssuePanelLabels,
status: BoardStatus
): string {
switch (status) {
case "running":
return labels.statusRunning;
case "pending_acceptance":
return labels.statusPendingAcceptance;
case "completed":
return labels.statusCompleted;
case "failed":
return labels.statusFailed;
case "canceled":
return labels.statusCanceled;
default:
return labels.statusNotStarted;
}
}
function statusDotClassName(status: BoardStatus): string {
switch (status) {
case "running":
return "bg-[var(--status-running)]";
case "pending_acceptance":
return "bg-[var(--tutti-purple)]";
case "completed":
return "bg-[var(--state-success)]";
case "failed":
return "bg-[var(--state-danger)]";
case "canceled":
return "bg-[var(--text-tertiary)]";
default:
return "bg-[var(--text-secondary)]";
}
}
type TuttiPlanIssueStage = {
kind: "parallel" | "sequential";
tasks: TuttiPlanIssueTaskSnapshot[];
};
/** Mirrors the dispatcher grouping: consecutive parallelizable tasks share a
* stage only while they stay independent of each other — a task that depends
* on a member of the running stage can never actually run alongside it
* (dependencies outrank the flag at dispatch), so it starts a new stage. */
export function groupTuttiPlanIssueTasksIntoStages(
tasks: readonly TuttiPlanIssueTaskSnapshot[]
): TuttiPlanIssueStage[] {
const ordered = [...tasks].sort(
(left, right) => left.sortIndex - right.sortIndex
);
const stages: TuttiPlanIssueStage[] = [];
for (const task of ordered) {
const previous = stages.at(-1);
if (
task.parallelizable &&
previous?.kind === "parallel" &&
!task.dependencyTaskIds.some((dependencyId) =>
previous.tasks.some((member) => member.taskId === dependencyId)
)
) {
previous.tasks.push(task);
continue;
}
stages.push({
kind: task.parallelizable ? "parallel" : "sequential",
tasks: [task]
});
}
return stages;
}
/** A task action that the source conversation turns into an Agent draft. */
export type TuttiPlanIssueTaskAction = TuttiModePlanTaskPromptAction;
/** A task that has launched has a delegate conversation to jump into. */
function taskHasConversation(task: TuttiPlanIssueTaskSnapshot): boolean {
return task.status !== "not_started";
}
/**
* Embedded "issue panel view" for the source conversation: once the accepted
* plan materialized an Issue, the conversation shows its subtasks as a live
* board/list. Pending and failed tasks can prepare accept/rework instructions
* in the source Agent composer; the panel itself does not mutate the managed
* Issue. A jump into the full Issue surface remains one click away.
*/
export function TuttiPlanIssuePanel({
embedded = false,
issue,
labels,
onOpenIssue,
onTaskAction,
onOpenTask
}: {
embedded?: boolean;
issue: TuttiPlanIssueSnapshot;
labels: TuttiPlanIssuePanelLabels;
onOpenIssue?: () => void;
onTaskAction?: (
taskId: string,
action: TuttiPlanIssueTaskAction
) => Promise<void>;
/** Jump into the delegate conversation of a task that has launched. */
onOpenTask?: (taskId: string) => void | Promise<void>;
}): React.JSX.Element {
const [viewMode, setViewMode] = useState<TuttiPlanIssueViewMode>("board");
const [decidingTaskIds, setDecidingTaskIds] = useState<readonly string[]>([]);
const requestTaskAction = onTaskAction
? (taskId: string, action: TuttiPlanIssueTaskAction): void => {
setDecidingTaskIds((current) =>
current.includes(taskId) ? current : [...current, taskId]
);
void onTaskAction(taskId, action)
.catch((error: unknown) => {
// Keep prompt preparation failures observable; otherwise a rejected
// host callback would read as a dead button.
console.error("tutti plan issue task prompt action failed", error);
})
.finally(() => {
setDecidingTaskIds((current) =>
current.filter((id) => id !== taskId)
);
});
}
: undefined;
const done = issue.tasks.filter((task) => task.status === "completed").length;
const running = issue.tasks.filter(
(task) => task.status === "running"
).length;
return (
<Card
size="sm"
className={cn(
"w-full pt-2!",
embedded && "border-0 bg-transparent shadow-none"
)}
data-testid="tutti-plan-issue-panel"
>
<CardHeader className="gap-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<ListChecks
aria-hidden
className="size-4 shrink-0 text-[var(--tutti-purple)]"
/>
<span className="min-w-0 truncate text-sm font-medium text-foreground">
{issue.title}
</span>
<span className="shrink-0 text-xs text-muted-foreground">
{labels.summary(
String(done),
String(issue.tasks.length),
String(running)
)}
</span>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<SegmentBar
segments={[
{
label: labels.listView,
testId: "tutti-plan-issue-view-list",
value: "list"
},
{
label: labels.boardView,
testId: "tutti-plan-issue-view-board",
value: "board"
}
]}
value={viewMode}
onValueChange={setViewMode}
/>
{onOpenIssue ? (
<Button
type="button"
variant="secondary"
data-testid="tutti-plan-issue-open"
onClick={onOpenIssue}
>
<ExternalLink aria-hidden className="size-3.5" />
{labels.openIssue}
</Button>
) : null}
</div>
</div>
</CardHeader>
<CardContent>
{viewMode === "board" ? (
<TuttiPlanIssueBoard
issue={issue}
labels={labels}
requestTaskAction={requestTaskAction}
decidingTaskIds={decidingTaskIds}
openTask={onOpenTask}
/>
) : (
<TuttiPlanIssueList
issue={issue}
labels={labels}
requestTaskAction={requestTaskAction}
decidingTaskIds={decidingTaskIds}
openTask={onOpenTask}
/>
)}
</CardContent>
</Card>
);
}
function TaskStructureChips({
labels,
task
}: {
labels: TuttiPlanIssuePanelLabels;
task: TuttiPlanIssueTaskSnapshot;
}): React.JSX.Element | null {
const dependencies = task.dependencyTaskIds.filter((id) => id.trim() !== "");
if (!task.parallelizable && !task.autoAccept && dependencies.length === 0) {
return null;
}
return (
<span className="flex min-w-0 flex-wrap items-center gap-1">
{task.parallelizable ? (
<Badge variant="accent" size="sm">
{labels.parallelizable}
</Badge>
) : null}
{task.autoAccept ? (
<Badge variant="success" size="sm">
{labels.autoAccept}
</Badge>
) : null}
{dependencies.length > 0 ? (
<Badge
variant="secondary"
size="sm"
className="min-w-0 truncate"
title={dependencies.join(", ")}
>
{labels.dependencies}: {dependencies.join(", ")}
</Badge>
) : null}
</span>
);
}
function TaskPromptActions({
labels,
task,
requestTaskAction,
deciding
}: {
labels: TuttiPlanIssuePanelLabels;
task: TuttiPlanIssueTaskSnapshot;
requestTaskAction?: (
taskId: string,
action: TuttiPlanIssueTaskAction
) => void;
deciding: boolean;
}): React.JSX.Element | null {
// These buttons only prepare source-Agent instructions. The Agent remains
// responsible for inspecting the authoritative execution before acting.
const canAccept = task.status === "pending_acceptance";
const canRework = canAccept || task.status === "failed";
if (!requestTaskAction || !canRework) {
return null;
}
return (
<span className="flex shrink-0 items-center gap-1">
{canAccept ? (
<Button
type="button"
size="sm"
className="h-6 bg-[var(--tutti-purple)] px-2 text-[11px] text-white hover:bg-[color-mix(in_srgb,var(--tutti-purple)_85%,black)]"
disabled={deciding}
data-testid={`tutti-plan-issue-accept-${task.taskId}`}
onClick={(event) => {
// The card itself jumps into the delegate conversation; preparing
// a source-Agent instruction must not trigger that navigation.
event.stopPropagation();
requestTaskAction(task.taskId, "accept");
}}
>
{labels.accept}
</Button>
) : null}
<Button
type="button"
size="sm"
variant="secondary"
className="h-6 px-2 text-[11px]"
disabled={deciding}
data-testid={`tutti-plan-issue-rework-${task.taskId}`}
onClick={(event) => {
event.stopPropagation();
requestTaskAction(task.taskId, "rework");
}}
>
<RotateCcw aria-hidden className="size-3.5" />
{labels.rework}
</Button>
</span>
);
}
function TuttiPlanIssueBoard({
issue,
labels,
requestTaskAction,
decidingTaskIds,
openTask
}: {
issue: TuttiPlanIssueSnapshot;
labels: TuttiPlanIssuePanelLabels;
requestTaskAction?: (
taskId: string,
action: TuttiPlanIssueTaskAction
) => void;
decidingTaskIds: readonly string[];
openTask?: (taskId: string) => void | Promise<void>;
}): React.JSX.Element {
const groups = new Map<BoardStatus, TuttiPlanIssueTaskSnapshot[]>();
for (const task of issue.tasks) {
const status = boardStatusOf(task);
groups.set(status, [...(groups.get(status) ?? []), task]);
}
const columns = BOARD_STATUS_ORDER.filter((status) =>
status === "failed" || status === "canceled"
? (groups.get(status)?.length ?? 0) > 0
: true
);
return (
<div className="board-scroll min-w-0 overflow-x-auto pb-1">
<div
className="grid gap-2"
style={{
gridTemplateColumns: `repeat(${columns.length}, minmax(280px, 1fr))`
}}
>
{columns.map((status) => {
const tasks = groups.get(status) ?? [];
return (
<div
key={status}
className="min-h-[220px] min-w-0 rounded-lg border border-[var(--line-2)] bg-muted/30 px-2 py-2"
data-testid={`tutti-plan-issue-column-${status}`}
>
<div className="mx-1 mb-1.5 flex items-center justify-between gap-2">
<span className="flex min-w-0 items-center gap-1.5">
<span
aria-hidden
className={cn(
"size-1.5 rounded-full",
statusDotClassName(status)
)}
/>
<span className="truncate text-[11px] font-medium text-foreground">
{statusLabel(labels, status)}
</span>
</span>
<span className="shrink-0 text-[11px] text-muted-foreground">
{tasks.length}
</span>
</div>
<div className="grid gap-1.5">
{tasks.map((task) => {
const openable = openTask && taskHasConversation(task);
return (
<div
key={task.taskId}
role={openable ? "button" : undefined}
tabIndex={openable ? 0 : undefined}
data-testid={`tutti-plan-issue-task-${task.taskId}`}
className={cn(
"min-w-0 overflow-hidden rounded-md bg-[var(--background-board-card)] p-3",
openable && "cursor-pointer"
)}
onClick={
openable ? () => void openTask(task.taskId) : undefined
}
onKeyDown={
openable
? (event) => {
if (event.key === "Enter") {
void openTask(task.taskId);
}
}
: undefined
}
>
<span className="line-clamp-2 text-[13px] font-medium text-foreground">
{task.title}
</span>
<span className="mt-2 block empty:hidden">
<TaskStructureChips labels={labels} task={task} />
</span>
<span className="mt-1.5 block empty:hidden">
<TaskPromptActions
labels={labels}
task={task}
requestTaskAction={requestTaskAction}
deciding={decidingTaskIds.includes(task.taskId)}
/>
</span>
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
);
}
function TuttiPlanIssueList({
issue,
labels,
requestTaskAction,
decidingTaskIds,
openTask
}: {
issue: TuttiPlanIssueSnapshot;
labels: TuttiPlanIssuePanelLabels;
requestTaskAction?: (
taskId: string,
action: TuttiPlanIssueTaskAction
) => void;
decidingTaskIds: readonly string[];
openTask?: (taskId: string) => void | Promise<void>;
}): React.JSX.Element {
const showStages = issue.tasks.some((task) => task.parallelizable);
const stages = showStages
? groupTuttiPlanIssueTasksIntoStages(issue.tasks)
: [{ kind: "sequential" as const, tasks: [...issue.tasks] }];
return (
<div className="overflow-hidden rounded-lg border border-[var(--line-2)] bg-[var(--background-board-card)]">
{stages.map((stage, index) => (
<div key={`stage-${index}`}>
{showStages ? (
<div
className="border-b border-[var(--line-2)] bg-muted/40 px-3 py-1 text-[10px] font-medium text-muted-foreground"
data-testid={`tutti-plan-issue-stage-${stage.kind}`}
>
{stage.kind === "parallel"
? labels.stageParallel(
String(index + 1),
String(stage.tasks.length)
)
: labels.stageSequential(String(index + 1))}
</div>
) : null}
{stage.tasks.map((task) => {
const status = boardStatusOf(task);
const openable = openTask && taskHasConversation(task);
return (
<div
key={task.taskId}
role={openable ? "button" : undefined}
tabIndex={openable ? 0 : undefined}
data-testid={`tutti-plan-issue-row-${task.taskId}`}
className={cn(
"flex items-start justify-between gap-3 border-b border-[var(--line-2)] px-3 py-2 last:border-b-0",
openable && "cursor-pointer"
)}
onClick={
openable ? () => void openTask(task.taskId) : undefined
}
onKeyDown={
openable
? (event) => {
if (event.key === "Enter") {
void openTask(task.taskId);
}
}
: undefined
}
>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
<span className="truncate text-[13px] font-medium text-foreground">
{task.title}
</span>
<TaskStructureChips labels={labels} task={task} />
</div>
{task.content ? (
<p className="mt-1 line-clamp-2 text-[11px] text-muted-foreground">
{task.content}
</p>
) : null}
</div>
<span className="flex shrink-0 items-center gap-2">
<TaskPromptActions
labels={labels}
task={task}
requestTaskAction={requestTaskAction}
deciding={decidingTaskIds.includes(task.taskId)}
/>
<span className="flex shrink-0 items-center gap-1.5 text-[11px] text-muted-foreground">
<span
aria-hidden
className={cn(
"size-1.5 rounded-full",
statusDotClassName(status)
)}
/>
{statusLabel(labels, status)}
</span>
</span>
</div>
);
})}
</div>
))}
</div>
);
}
@@ -0,0 +1,7 @@
export * from "./tuttiModePlanPanelProjection";
export * from "./tuttiModePlanTaskAssignments";
export * from "./tuttiModePlanPromptActions";
export * from "./TuttiModePlanPanel";
export * from "./TuttiModePlanTaskAssignmentEditor";
export * from "./TuttiPlanIssuePanel";
export * from "./useTuttiModePlanPanels";
@@ -0,0 +1,344 @@
import { describe, expect, it } from "vitest";
import {
projectTuttiModePlanPanel,
type TuttiModePlanReviewSnapshot
} from "./tuttiModePlanPanelProjection";
function planSnapshot(
overrides: Partial<TuttiModePlanReviewSnapshot> = {}
): TuttiModePlanReviewSnapshot {
return {
workflow: {
id: "workflow-1",
workspaceId: "workspace-1",
type: "tutti_mode_plan",
owner: "tutti",
triggerKind: "agent_cli",
sourceSessionId: "session-1",
sourceTurnId: "turn-1",
sourceToolCallId: "tool-call-1",
status: "pending_review",
currentRevisionId: "revision-1"
},
revisions: [
{
id: "revision-1",
workflowId: "workflow-1",
sequence: 1,
schemaVersion: "tutti-mode-plan/v1",
documentPath: `tutti-mode-plans/workflow-1/revisions/${"a".repeat(64)}.md`,
sha256:
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
producedByTurnId: "turn-1",
createdAtUnixMs: 110,
document: {
schema: "tutti-mode-plan/v1",
phase: "task_graph",
title: "Ship the workflow",
topicId: "topic-1",
markdownBody: "## Goal\n\nShip the durable workflow safely.",
execution: {
mode: "sequential",
reasoningIntensity: 70,
orchestrationIntensity: 60
},
budget: {
mode: "fixed",
tokenLimit: 120_000,
quotaWaterlinePercent: 15
},
tasks: [
{
id: "design",
title: "Design",
content: "Define contracts",
priority: "high",
agentTargetId: "local:codex",
modelPlanId: "model-plan-1",
model: "gpt-5.4",
permissionModeId: "acceptEdits",
reasoningEffort: "high",
executionDirectory: "packages/agent/gui",
dependsOn: [],
parallelizable: true,
autoAccept: true
},
{
id: "build",
title: "Build",
content: "Implement the contracts",
priority: "medium",
agentTargetId: null,
modelPlanId: null,
model: null,
permissionModeId: null,
reasoningEffort: null,
executionDirectory: null,
dependsOn: ["design"]
}
]
}
}
],
checkpoints: [
{
id: "checkpoint-1",
workflowId: "workflow-1",
kind: "task_review",
revisionId: "revision-1",
status: "pending",
decidedBy: null,
decisionReason: null,
createdAtUnixMs: 120,
updatedAtUnixMs: 120,
decidedAtUnixMs: null
}
],
...overrides
};
}
describe("projectTuttiModePlanPanel", () => {
it("projects the single pending plan review from durable workflow state", () => {
expect(projectTuttiModePlanPanel(planSnapshot())).toEqual({
id: "workflow-1:checkpoint-1",
workflowId: "workflow-1",
workspaceId: "workspace-1",
sourceSessionId: "session-1",
sourceTurnId: "turn-1",
sourceToolCallId: "tool-call-1",
reviewKind: "task_review",
state: "pending",
actionable: true,
title: "Ship the workflow",
topicId: "topic-1",
markdownBody: "## Goal\n\nShip the durable workflow safely.",
revision: {
id: "revision-1",
sequence: 1,
schemaVersion: "tutti-mode-plan/v1",
documentPath: `tutti-mode-plans/workflow-1/revisions/${"a".repeat(64)}.md`,
sha256:
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
producedByTurnId: "turn-1",
createdAtUnixMs: 110
},
checkpoint: {
id: "checkpoint-1",
status: "pending",
decidedBy: null,
decisionReason: null,
decidedAtUnixMs: null,
createdAtUnixMs: 120,
updatedAtUnixMs: 120
},
execution: {
mode: "sequential",
reasoningIntensity: 70,
orchestrationIntensity: 60
},
budget: {
mode: "fixed",
tokenLimit: 120_000,
quotaWaterlinePercent: 15
},
tasks: [
{
ordinal: 1,
id: "design",
title: "Design",
content: "Define contracts",
priority: "high",
agentTargetId: "local:codex",
modelPlanId: "model-plan-1",
model: "gpt-5.4",
permissionModeId: "acceptEdits",
reasoningEffort: "high",
executionDirectory: "packages/agent/gui",
dependsOn: [],
parallelizable: true,
autoAccept: true
},
{
ordinal: 2,
id: "build",
title: "Build",
content: "Implement the contracts",
priority: "medium",
agentTargetId: null,
modelPlanId: null,
model: null,
permissionModeId: null,
reasoningEffort: null,
executionDirectory: null,
dependsOn: ["design"],
parallelizable: false,
autoAccept: false
}
]
});
});
it("never projects a legacy configuration review", () => {
const legacy = planSnapshot();
legacy.revisions[0]!.document.phase = "configuration";
legacy.revisions[0]!.document.tasks = [];
legacy.checkpoints[0] = {
...legacy.checkpoints[0]!,
kind: "configuration_review"
};
expect(projectTuttiModePlanPanel(legacy)).toBeNull();
});
it("projects only the current revision", () => {
const snapshot = planSnapshot();
snapshot.workflow.currentRevisionId = "revision-2";
snapshot.revisions = [
...snapshot.revisions,
{
...snapshot.revisions[0]!,
id: "revision-2",
sequence: 2,
producedByTurnId: "turn-2",
createdAtUnixMs: 210,
document: {
...snapshot.revisions[0]!.document,
markdownBody: "## Tasks\n\nImplement and verify the workflow."
}
}
];
snapshot.checkpoints = [
{
...snapshot.checkpoints[0]!,
status: "superseded",
updatedAtUnixMs: 205
},
{
id: "checkpoint-2",
workflowId: "workflow-1",
kind: "task_review",
revisionId: "revision-2",
status: "accepted",
decidedBy: "user-1",
decisionReason: "Ready to execute",
createdAtUnixMs: 220,
updatedAtUnixMs: 230,
decidedAtUnixMs: 230
}
];
const panel = projectTuttiModePlanPanel(snapshot);
expect(panel).toMatchObject({
id: "workflow-1:checkpoint-2",
reviewKind: "task_review",
state: "accepted",
actionable: false,
revision: { id: "revision-2", sequence: 2 },
checkpoint: {
id: "checkpoint-2",
decisionReason: "Ready to execute"
}
});
});
it.each([
["pending", "pending", true],
["accepted", "accepted", false],
["rejected", "rejected", false],
["canceled", "canceled", false],
["superseded", "expired", false]
] as const)(
"maps checkpoint status %s to panel state %s",
(checkpointStatus, panelState, actionable) => {
const snapshot = planSnapshot();
snapshot.checkpoints[0] = {
...snapshot.checkpoints[0]!,
status: checkpointStatus
};
expect(projectTuttiModePlanPanel(snapshot)).toMatchObject({
state: panelState,
actionable
});
}
);
it("uses the newest checkpoint for the current immutable revision", () => {
const snapshot = planSnapshot();
snapshot.checkpoints.push({
...snapshot.checkpoints[0]!,
id: "checkpoint-2",
status: "rejected",
decisionReason: "Revise the budget",
updatedAtUnixMs: 150
});
expect(projectTuttiModePlanPanel(snapshot)).toMatchObject({
id: "workflow-1:checkpoint-2",
state: "rejected",
checkpoint: {
id: "checkpoint-2",
decisionReason: "Revise the budget"
}
});
});
it("fails closed when the current revision or its matching review is missing", () => {
const missingRevision = planSnapshot();
missingRevision.workflow.currentRevisionId = "revision-missing";
expect(projectTuttiModePlanPanel(missingRevision)).toBeNull();
const missingCheckpoint = planSnapshot();
missingCheckpoint.checkpoints = [];
expect(projectTuttiModePlanPanel(missingCheckpoint)).toBeNull();
});
it("fails closed when durable checkpoint kind and parsed Markdown phase disagree", () => {
const snapshot = planSnapshot();
snapshot.revisions[0]!.document.phase = "configuration";
snapshot.revisions[0]!.document.tasks = [];
expect(projectTuttiModePlanPanel(snapshot)).toBeNull();
});
it("does not derive a panel from transcript markers or agent interactions", () => {
const nonTuttiSnapshot = {
...planSnapshot(),
workflow: {
...planSnapshot().workflow,
type: "provider_plan"
},
transcript: "<!-- legacy-provider-plan -->",
pendingInteractions: [{ kind: "exit_plan" }]
} as unknown as TuttiModePlanReviewSnapshot;
expect(projectTuttiModePlanPanel(nonTuttiSnapshot)).toBeNull();
});
it("does not expose mutable arrays from the source snapshot", () => {
const snapshot = planSnapshot();
const panel = projectTuttiModePlanPanel(snapshot);
expect(panel).not.toBeNull();
panel?.tasks.push({
ordinal: 3,
id: "local-only",
title: "Local only",
content: "",
priority: "low",
agentTargetId: null,
modelPlanId: null,
model: null,
permissionModeId: null,
reasoningEffort: null,
executionDirectory: null,
dependsOn: [],
parallelizable: false,
autoAccept: false
});
expect(snapshot.revisions[0]?.document.tasks).toHaveLength(2);
});
});
@@ -0,0 +1,328 @@
export type TuttiModePlanWorkflowStatus =
| "pending_review"
| "in_progress"
| "accepted"
| "rejected"
| "completed"
| "failed"
| "canceled";
export type TuttiModePlanCheckpointKind =
| "configuration_review"
| "task_review";
export type TuttiModePlanCheckpointStatus =
| "pending"
| "accepted"
| "rejected"
| "superseded"
| "canceled";
// UI-facing review snapshot. It deliberately excludes workflow operations,
// turn links, actionable items, and unrelated timestamps from the daemon DTO.
// Desktop must explicitly map the transport contract into this narrow seam.
export interface TuttiModePlanReviewSnapshot {
workflow: TuttiModePlanWorkflow;
revisions: TuttiModePlanRevision[];
checkpoints: TuttiModePlanCheckpoint[];
}
export interface TuttiModePlanWorkflow {
id: string;
workspaceId: string;
type: string;
owner: string;
triggerKind: string;
sourceSessionId: string;
sourceTurnId?: string | null;
sourceToolCallId?: string | null;
status: TuttiModePlanWorkflowStatus;
currentRevisionId: string;
}
export interface TuttiModePlanRevision {
id: string;
workflowId: string;
sequence: number;
schemaVersion: string;
documentPath: string;
sha256: string;
producedByTurnId?: string | null;
createdAtUnixMs: number;
document: TuttiModePlanDocument;
}
export interface TuttiModePlanDocument {
schema: string;
phase: "configuration" | "task_graph";
title: string;
topicId: string;
markdownBody: string;
execution: TuttiModePlanExecution;
budget: TuttiModePlanBudget;
tasks: TuttiModePlanTask[];
}
export interface TuttiModePlanExecution {
mode: "sequential" | "parallel";
reasoningIntensity: number;
orchestrationIntensity: number;
effect?: number | null;
speed?: number | null;
}
export interface TuttiModePlanBudget {
mode: "auto" | "fixed";
tokenLimit: number;
quotaWaterlinePercent: number;
}
export interface TuttiModePlanTask {
id: string;
title: string;
content: string;
priority: "high" | "medium" | "low";
agentTargetId?: string | null;
modelPlanId?: string | null;
model?: string | null;
permissionModeId?: string | null;
reasoningEffort?: string | null;
executionDirectory?: string | null;
dependsOn: string[];
parallelizable?: boolean;
autoAccept?: boolean;
}
export interface TuttiModePlanCheckpoint {
id: string;
workflowId: string;
kind: TuttiModePlanCheckpointKind;
revisionId: string;
status: TuttiModePlanCheckpointStatus;
decidedBy?: string | null;
decisionReason?: string | null;
createdAtUnixMs: number;
updatedAtUnixMs: number;
decidedAtUnixMs?: number | null;
}
export type TuttiModePlanPanelState =
| "pending"
| "accepted"
| "rejected"
| "canceled"
| "expired";
export interface TuttiModePlanPanelRevisionViewModel {
id: string;
sequence: number;
schemaVersion: string;
documentPath: string;
sha256: string;
producedByTurnId: string | null;
createdAtUnixMs: number;
}
export interface TuttiModePlanPanelCheckpointViewModel {
id: string;
status: TuttiModePlanCheckpointStatus;
decidedBy: string | null;
decisionReason: string | null;
decidedAtUnixMs: number | null;
createdAtUnixMs: number;
updatedAtUnixMs: number;
}
export interface TuttiModePlanPanelTaskViewModel {
ordinal: number;
id: string;
title: string;
content: string;
priority: "high" | "medium" | "low";
agentTargetId: string | null;
modelPlanId: string | null;
model: string | null;
permissionModeId: string | null;
reasoningEffort: string | null;
executionDirectory: string | null;
dependsOn: string[];
parallelizable: boolean;
autoAccept: boolean;
}
export interface TuttiModePlanPanelViewModel {
id: string;
workflowId: string;
workspaceId: string;
sourceSessionId: string;
sourceTurnId: string | null;
sourceToolCallId: string | null;
reviewKind: TuttiModePlanCheckpointKind;
state: TuttiModePlanPanelState;
actionable: boolean;
title: string;
topicId: string;
markdownBody: string;
revision: TuttiModePlanPanelRevisionViewModel;
checkpoint: TuttiModePlanPanelCheckpointViewModel;
execution: TuttiModePlanExecution;
budget: TuttiModePlanBudget;
tasks: TuttiModePlanPanelTaskViewModel[];
}
function nullableString(value: string | null | undefined): string | null {
const normalized = value?.trim();
return normalized ? normalized : null;
}
function checkpointSortValue(
left: TuttiModePlanCheckpoint,
right: TuttiModePlanCheckpoint
): number {
return (
left.updatedAtUnixMs - right.updatedAtUnixMs ||
left.createdAtUnixMs - right.createdAtUnixMs ||
left.id.localeCompare(right.id)
);
}
function currentCheckpoint(
snapshot: TuttiModePlanReviewSnapshot,
revision: TuttiModePlanRevision
): TuttiModePlanCheckpoint | null {
const checkpoints = snapshot.checkpoints
.filter(
(candidate) =>
candidate.workflowId === snapshot.workflow.id &&
candidate.revisionId === revision.id
)
.sort(checkpointSortValue);
return checkpoints.at(-1) ?? null;
}
// The single-review flow renders only the task review over a complete plan
// document. Legacy two-phase configuration checkpoints are retired by the
// daemon at startup and never projected.
function reviewKindMatchesDocument(
kind: TuttiModePlanCheckpointKind,
phase: TuttiModePlanDocument["phase"]
): boolean {
return kind === "task_review" && phase === "task_graph";
}
function panelState(
workflowStatus: TuttiModePlanWorkflowStatus,
checkpointStatus: TuttiModePlanCheckpointStatus
): TuttiModePlanPanelState {
switch (checkpointStatus) {
case "accepted":
case "rejected":
case "canceled":
return checkpointStatus;
case "superseded":
return "expired";
case "pending":
if (workflowStatus === "canceled") return "canceled";
if (workflowStatus === "rejected") return "rejected";
if (workflowStatus === "failed") return "expired";
if (workflowStatus === "accepted" || workflowStatus === "completed") {
return "accepted";
}
return "pending";
}
}
function projectTask(
task: TuttiModePlanTask,
index: number
): TuttiModePlanPanelTaskViewModel {
return {
ordinal: index + 1,
id: task.id,
title: task.title,
content: task.content,
priority: task.priority,
agentTargetId: nullableString(task.agentTargetId),
modelPlanId: nullableString(task.modelPlanId),
model: nullableString(task.model),
permissionModeId: nullableString(task.permissionModeId),
reasoningEffort: nullableString(task.reasoningEffort),
executionDirectory: nullableString(task.executionDirectory),
dependsOn: [...task.dependsOn],
parallelizable: task.parallelizable === true,
autoAccept: task.autoAccept === true
};
}
/**
* Projects an authoritative, normalized workflow snapshot into the Tutti Mode
* Plan review panel. The input is supplied by the workflow host boundary; this
* function never reads transcript messages, provider interactions, or revision
* files.
*/
export function projectTuttiModePlanPanel(
snapshot: TuttiModePlanReviewSnapshot
): TuttiModePlanPanelViewModel | null {
const workflow = snapshot.workflow;
if (
workflow.type !== "tutti_mode_plan" ||
workflow.owner !== "tutti" ||
workflow.triggerKind !== "agent_cli"
) {
return null;
}
const revision = snapshot.revisions.find(
(candidate) =>
candidate.id === workflow.currentRevisionId &&
candidate.workflowId === workflow.id
);
if (!revision || revision.document.schema !== revision.schemaVersion) {
return null;
}
const checkpoint = currentCheckpoint(snapshot, revision);
if (
!checkpoint ||
!reviewKindMatchesDocument(checkpoint.kind, revision.document.phase)
) {
return null;
}
const state = panelState(workflow.status, checkpoint.status);
return {
id: `${workflow.id}:${checkpoint.id}`,
workflowId: workflow.id,
workspaceId: workflow.workspaceId,
sourceSessionId: workflow.sourceSessionId,
sourceTurnId: nullableString(workflow.sourceTurnId),
sourceToolCallId: nullableString(workflow.sourceToolCallId),
reviewKind: checkpoint.kind,
state,
actionable: state === "pending",
title: revision.document.title,
topicId: revision.document.topicId,
markdownBody: revision.document.markdownBody,
revision: {
id: revision.id,
sequence: revision.sequence,
schemaVersion: revision.schemaVersion,
documentPath: revision.documentPath,
sha256: revision.sha256,
producedByTurnId: nullableString(revision.producedByTurnId),
createdAtUnixMs: revision.createdAtUnixMs
},
checkpoint: {
id: checkpoint.id,
status: checkpoint.status,
decidedBy: nullableString(checkpoint.decidedBy),
decisionReason: nullableString(checkpoint.decisionReason),
decidedAtUnixMs: checkpoint.decidedAtUnixMs ?? null,
createdAtUnixMs: checkpoint.createdAtUnixMs,
updatedAtUnixMs: checkpoint.updatedAtUnixMs
},
execution: { ...revision.document.execution },
budget: { ...revision.document.budget },
tasks: revision.document.tasks.map(projectTask)
};
}
@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import type { TuttiPlanIssueSnapshot } from "../workspaceWorkflowRuntime";
import { tuttiModePlanTaskActionPrompt } from "./tuttiModePlanPromptActions";
const issue: TuttiPlanIssueSnapshot = {
workflowId: "workflow-1",
sourceTurnId: "turn-1",
issueId: "issue-1",
topicId: "topic/one",
title: "Plan",
dispatchPaused: false,
tasks: [
{
taskId: "task-1",
title: "Review [result]",
content: "",
status: "failed",
sortIndex: 1,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
}
]
};
const labels = {
accept: (reference: string) => `Accept ${reference}`,
rework: (reference: string) => `Rework ${reference}`
};
describe("tuttiModePlanTaskActionPrompt", () => {
it.each([
["accept", "Accept"],
["rework", "Rework"]
] as const)(
"builds an exact source-Agent %s draft with a task mention",
(action, prefix) => {
expect(
tuttiModePlanTaskActionPrompt({
action,
issue,
labels,
taskId: "task-1",
workspaceId: "workspace-1"
})
).toBe(
`${prefix} [Review \\[result\\]](mention://workspace-issue/issue-1?workspaceId=workspace-1&topicId=topic%2Fone&taskId=task-1)`
);
}
);
it("fails closed when the task is no longer in the current projection", () => {
expect(
tuttiModePlanTaskActionPrompt({
action: "rework",
issue,
labels,
taskId: "missing",
workspaceId: "workspace-1"
})
).toBeNull();
});
});
@@ -0,0 +1,64 @@
import { buildWorkspaceIssueMentionHref } from "@tutti-os/workspace-issue-manager/core";
import type {
TuttiPlanIssueSnapshot,
TuttiPlanIssueTaskSnapshot
} from "../workspaceWorkflowRuntime";
export type TuttiModePlanTaskPromptAction = "accept" | "rework";
export interface TuttiModePlanTaskPromptLabels {
accept(reference: string): string;
rework(reference: string): string;
}
function escapeMarkdownLabel(label: string): string {
return label
.replaceAll("\\", "\\\\")
.replaceAll("[", "\\[")
.replaceAll("]", "\\]");
}
function taskReference(input: {
issue: TuttiPlanIssueSnapshot;
task: TuttiPlanIssueTaskSnapshot;
workspaceId: string;
}): string {
const href = buildWorkspaceIssueMentionHref({
issueId: input.issue.issueId,
taskId: input.task.taskId,
topicId: input.issue.topicId,
workspaceId: input.workspaceId
});
const label = input.task.title.trim() || input.task.taskId;
return href
? `[${escapeMarkdownLabel(label)}](${href})`
: escapeMarkdownLabel(label);
}
/**
* Converts a plan-panel user action into a source-Agent draft prompt.
*
* This is intentionally presentation-only: it does not claim Tutti execution
* authority or mutate a managed Issue. Sending the resulting draft lets the
* source Agent inspect the canonical execution and use its fenced commands.
*/
export function tuttiModePlanTaskActionPrompt(input: {
action: TuttiModePlanTaskPromptAction;
issue: TuttiPlanIssueSnapshot;
labels: TuttiModePlanTaskPromptLabels;
taskId: string;
workspaceId: string;
}): string | null {
const task = input.issue.tasks.find(
(candidate) => candidate.taskId === input.taskId
);
if (!task) {
return null;
}
const reference = taskReference({
issue: input.issue,
task,
workspaceId: input.workspaceId
});
return input.labels[input.action](reference).trim() || null;
}
@@ -0,0 +1,160 @@
import { describe, expect, it } from "vitest";
import {
effectiveTaskAssignmentValue,
effectiveTaskFlag,
mergeTaskAssignmentDraft,
taskAssignmentInputsFromDrafts
} from "./tuttiModePlanTaskAssignments";
import type { TuttiModePlanPanelTaskViewModel } from "./tuttiModePlanPanelProjection";
function taskViewModel(
overrides: Partial<TuttiModePlanPanelTaskViewModel> & { id: string }
): TuttiModePlanPanelTaskViewModel {
return {
ordinal: 1,
title: "Task",
content: "",
priority: "medium",
agentTargetId: null,
modelPlanId: null,
model: null,
permissionModeId: null,
reasoningEffort: null,
executionDirectory: null,
dependsOn: [],
parallelizable: false,
autoAccept: false,
...overrides
};
}
describe("mergeTaskAssignmentDraft", () => {
it("stores field edits per task", () => {
const drafts = mergeTaskAssignmentDraft({}, "task-1", {
reasoningEffort: "high"
});
expect(drafts["task-1"]).toEqual({ reasoningEffort: "high" });
});
it("resets dependent selections when the agent changes", () => {
let drafts = mergeTaskAssignmentDraft({}, "task-1", {
modelPlanId: "plan-1",
model: "model-1"
});
drafts = mergeTaskAssignmentDraft(drafts, "task-1", {
agentTargetId: "agent-2"
});
expect(drafts["task-1"]).toEqual({
agentTargetId: "agent-2",
modelPlanId: "",
model: "",
permissionModeId: "",
reasoningEffort: ""
});
});
it("keeps the agent-independent toggles across agent changes", () => {
let drafts = mergeTaskAssignmentDraft({}, "task-1", {
parallelizable: true
});
drafts = mergeTaskAssignmentDraft(drafts, "task-1", { autoAccept: true });
drafts = mergeTaskAssignmentDraft(drafts, "task-1", {
agentTargetId: "agent-2"
});
expect(drafts["task-1"]).toEqual({
agentTargetId: "agent-2",
modelPlanId: "",
model: "",
permissionModeId: "",
reasoningEffort: "",
parallelizable: true,
autoAccept: true
});
expect(effectiveTaskFlag(undefined, true)).toBe(true);
expect(effectiveTaskFlag(false, true)).toBe(false);
const inputs = taskAssignmentInputsFromDrafts(drafts, [
taskViewModel({ id: "task-1" })
]);
expect(inputs).toEqual([
{
taskId: "task-1",
agentTargetId: "agent-2",
modelPlanId: "",
model: "",
permissionModeId: "",
reasoningEffort: "",
parallelizable: true,
autoAccept: true
}
]);
});
it("keeps other fields when the same agent is re-selected", () => {
let drafts = mergeTaskAssignmentDraft({}, "task-1", {
agentTargetId: "agent-1"
});
drafts = mergeTaskAssignmentDraft(drafts, "task-1", { model: "model-9" });
drafts = mergeTaskAssignmentDraft(drafts, "task-1", {
agentTargetId: "agent-1"
});
expect(drafts["task-1"]?.model).toBe("model-9");
});
});
describe("effectiveTaskAssignmentValue", () => {
it("prefers the draft edit, including an explicit clear", () => {
expect(effectiveTaskAssignmentValue(undefined, "doc-value")).toBe(
"doc-value"
);
expect(effectiveTaskAssignmentValue("edit", "doc-value")).toBe("edit");
expect(effectiveTaskAssignmentValue("", "doc-value")).toBe("");
expect(effectiveTaskAssignmentValue(undefined, null)).toBe("");
});
});
describe("taskAssignmentInputsFromDrafts", () => {
it("sends only touched tasks and touched fields", () => {
const tasks = [
taskViewModel({ id: "task-1" }),
taskViewModel({ id: "task-2", ordinal: 2 })
];
const drafts = mergeTaskAssignmentDraft({}, "task-2", {
reasoningEffort: "high"
});
const inputs = taskAssignmentInputsFromDrafts(drafts, tasks);
expect(inputs).toEqual([{ taskId: "task-2", reasoningEffort: "high" }]);
expect("model" in inputs[0]!).toBe(false);
});
it("carries the dependent clears produced by an agent change", () => {
const tasks = [taskViewModel({ id: "task-1" })];
const drafts = mergeTaskAssignmentDraft({}, "task-1", {
agentTargetId: "agent-1"
});
expect(taskAssignmentInputsFromDrafts(drafts, tasks)).toEqual([
{
taskId: "task-1",
agentTargetId: "agent-1",
modelPlanId: "",
model: "",
permissionModeId: "",
reasoningEffort: ""
}
]);
});
it("drops drafts for tasks missing from the current revision", () => {
const drafts = mergeTaskAssignmentDraft({}, "task-gone", {
model: "model-1"
});
expect(
taskAssignmentInputsFromDrafts(drafts, [taskViewModel({ id: "task-1" })])
).toEqual([]);
});
it("returns an empty payload for untouched drafts", () => {
expect(
taskAssignmentInputsFromDrafts({}, [taskViewModel({ id: "task-1" })])
).toEqual([]);
});
});
@@ -0,0 +1,123 @@
import type { TuttiModePlanTaskAssignmentInput } from "../workspaceWorkflowRuntime";
import type { TuttiModePlanPanelTaskViewModel } from "./tuttiModePlanPanelProjection";
/**
* One task's in-panel assignment edits. `undefined` means "untouched — keep
* the plan document value"; an empty string is an explicit clear that the
* daemon persists as such.
*/
export interface TuttiModePlanTaskAssignmentDraft {
agentTargetId?: string;
modelPlanId?: string;
model?: string;
permissionModeId?: string;
reasoningEffort?: string;
/** Per-task parallel opt-in; undefined keeps the plan document value. */
parallelizable?: boolean;
/** Per-task acceptance bypass; undefined keeps the plan document value. */
autoAccept?: boolean;
}
export type TuttiModePlanTaskAssignmentDrafts = Readonly<
Record<string, TuttiModePlanTaskAssignmentDraft>
>;
/**
* Applies one field edit. Changing the Agent invalidates the dependent
* selections (plan, model, permission mode, reasoning effort) because their
* option catalogs are agent-scoped; they reset to an explicit clear so an
* incompatible document value can never ride along silently. The parallel
* opt-in is agent-independent and survives an agent change.
*/
export function mergeTaskAssignmentDraft(
drafts: TuttiModePlanTaskAssignmentDrafts,
taskId: string,
patch: TuttiModePlanTaskAssignmentDraft
): TuttiModePlanTaskAssignmentDrafts {
const current = drafts[taskId] ?? {};
const next: TuttiModePlanTaskAssignmentDraft =
patch.agentTargetId !== undefined &&
patch.agentTargetId !== current.agentTargetId
? {
agentTargetId: patch.agentTargetId,
modelPlanId: "",
model: "",
permissionModeId: "",
reasoningEffort: "",
...(current.parallelizable !== undefined
? { parallelizable: current.parallelizable }
: {}),
...(current.autoAccept !== undefined
? { autoAccept: current.autoAccept }
: {})
}
: { ...current, ...patch };
return { ...drafts, [taskId]: next };
}
/** The state a boolean toggle (parallel, auto-accept) displays: draft wins. */
export function effectiveTaskFlag(
draftValue: boolean | undefined,
documentValue: boolean
): boolean {
return draftValue ?? documentValue;
}
/** The value a selector should display: draft edit wins over document value. */
export function effectiveTaskAssignmentValue(
draftValue: string | undefined,
documentValue: string | null
): string {
if (draftValue !== undefined) return draftValue;
return documentValue ?? "";
}
/**
* Builds the decision payload. Only touched tasks are included, and only
* touched fields are sent; untouched fields stay null-equivalent (omitted) so
* the daemon keeps the plan document value.
*/
export function taskAssignmentInputsFromDrafts(
drafts: TuttiModePlanTaskAssignmentDrafts,
tasks: readonly TuttiModePlanPanelTaskViewModel[]
): TuttiModePlanTaskAssignmentInput[] {
const knownTaskIds = new Set(tasks.map((task) => task.id));
const inputs: TuttiModePlanTaskAssignmentInput[] = [];
for (const task of tasks) {
const draft = drafts[task.id];
if (!draft) continue;
const input: TuttiModePlanTaskAssignmentInput = { taskId: task.id };
let touched = false;
if (draft.agentTargetId !== undefined) {
input.agentTargetId = draft.agentTargetId;
touched = true;
}
if (draft.modelPlanId !== undefined) {
input.modelPlanId = draft.modelPlanId;
touched = true;
}
if (draft.model !== undefined) {
input.model = draft.model;
touched = true;
}
if (draft.permissionModeId !== undefined) {
input.permissionModeId = draft.permissionModeId;
touched = true;
}
if (draft.reasoningEffort !== undefined) {
input.reasoningEffort = draft.reasoningEffort;
touched = true;
}
if (draft.parallelizable !== undefined) {
input.parallelizable = draft.parallelizable;
touched = true;
}
if (draft.autoAccept !== undefined) {
input.autoAccept = draft.autoAccept;
touched = true;
}
if (touched) inputs.push(input);
}
// Drafts for tasks that vanished from the current revision are dropped.
return inputs.filter((input) => knownTaskIds.has(input.taskId));
}
@@ -0,0 +1,307 @@
import "@testing-library/jest-dom/vitest";
import { StrictMode } from "react";
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import {
tuttiPlanIssueExecutionIsActive,
useTuttiModePlanPanels
} from "./useTuttiModePlanPanels";
import {
TuttiModePlanReviewRuntimeProvider,
type TuttiModePlanReviewRuntime,
type TuttiPlanIssueQueryResult,
type TuttiPlanIssueSnapshot,
type TuttiPlanIssueSource
} from "../workspaceWorkflowRuntime";
function snapshotFor(sessionId: string): TuttiPlanIssueSnapshot {
return {
workflowId: `workflow-${sessionId}`,
sourceTurnId: `turn-${sessionId}`,
issueId: `tutti-mode-plan-${sessionId}`,
topicId: "default",
title: `Issue for ${sessionId}`,
dispatchPaused: false,
tasks: [
{
taskId: "task-1",
title: "Only",
content: "",
status: "running",
sortIndex: 1,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
}
]
};
}
describe("tuttiPlanIssueExecutionIsActive", () => {
it.each(["not_started", "running", "pending_acceptance"])(
"treats %s work as active while dispatch is open",
(status) => {
const issue = snapshotFor("session-1");
expect(
tuttiPlanIssueExecutionIsActive({
...issue,
tasks: [{ ...issue.tasks[0]!, status }]
})
).toBe(true);
}
);
it("ends aggregate work when dispatch is paused or every task is terminal", () => {
const issue = snapshotFor("session-1");
expect(
tuttiPlanIssueExecutionIsActive({ ...issue, dispatchPaused: true })
).toBe(false);
expect(
tuttiPlanIssueExecutionIsActive({
...issue,
tasks: ["completed", "failed", "canceled"].map((status, index) => ({
...issue.tasks[0]!,
taskId: `task-${index}`,
status
}))
})
).toBe(false);
});
});
interface DeferredLoad {
sessionId: string;
resolve: (result: TuttiPlanIssueQueryResult) => void;
}
function createHarness(): {
runtime: TuttiModePlanReviewRuntime;
loads: DeferredLoad[];
emitIssueUpdate: (issueId: string) => void;
emitWorkflowUpdate: (sourceSessionId: string) => void;
emitSessionSettled: (sourceSessionId: string) => void;
} {
const loads: DeferredLoad[] = [];
const listeners = new Set<(update: { issueId: string }) => void>();
const workflowListeners = new Set<
Parameters<TuttiModePlanReviewRuntime["subscribe"]>[1]
>();
const source: TuttiPlanIssueSource = {
getSessionPlanIssue({ sourceSessionId }) {
return new Promise((resolve) => {
loads.push({ sessionId: sourceSessionId, resolve });
});
},
subscribeIssueUpdates(_workspaceId, listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
cancelExecution: vi.fn().mockResolvedValue(undefined),
resolveTaskSession: vi.fn().mockResolvedValue(null)
};
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn().mockResolvedValue([]),
decide: vi.fn().mockResolvedValue(undefined),
subscribe: (_workspaceId, listener) => {
workflowListeners.add(listener);
return () => workflowListeners.delete(listener);
},
planIssues: source
};
return {
runtime,
loads,
emitIssueUpdate: (issueId) => {
for (const listener of listeners) listener({ issueId });
},
emitWorkflowUpdate: (sourceSessionId) => {
for (const listener of workflowListeners) {
listener({
kind: "workflow_updated",
workspaceId: "workspace-1",
workflowId: "workflow-1",
sourceSessionId,
checkpointId: "checkpoint-1",
changeKind: "operation_updated"
});
}
},
emitSessionSettled: (sourceSessionId) => {
for (const listener of workflowListeners) {
listener({
kind: "session_settled",
workspaceId: "workspace-1",
sourceSessionId
});
}
}
};
}
function Probe({ sessionId }: { sessionId: string }): React.JSX.Element {
const { planIssue } = useTuttiModePlanPanels({
decidedBy: "user-1",
workspaceId: "workspace-1",
sourceSessionId: sessionId
});
return (
<div data-testid="probe">{planIssue ? planIssue.issueId : "none"}</div>
);
}
// The plan-issue embed rides the review panels hook lifecycle (one scope, one
// effect); these probes pin the load/refresh semantics the embedded panel
// depends on.
describe("useTuttiModePlanPanels plan issue embed", () => {
it("surfaces the loaded issue under StrictMode double-mount", async () => {
const harness = createHarness();
render(
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
);
await waitFor(() => expect(harness.loads.length).toBeGreaterThan(0));
for (const load of [...harness.loads]) {
load.resolve({ kind: "issue", issue: snapshotFor(load.sessionId) });
}
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-a"
)
);
});
it("re-reads the plan issue on workflow updates for this session", async () => {
// workspace.issue.updated fires during materialization, before the
// create_issue operation records its outcome; the workflow's
// operation_updated event is the authoritative post-completion signal.
// Without this re-read, an accepted plan's issue panel stays invisible
// until a remount.
const harness = createHarness();
render(
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
);
await waitFor(() => expect(harness.loads.length).toBe(1));
harness.loads[0]!.resolve(null);
harness.emitWorkflowUpdate("session-a");
await waitFor(() => expect(harness.loads.length).toBe(2));
harness.loads[1]!.resolve({
kind: "issue",
issue: snapshotFor("session-a")
});
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-a"
)
);
// Another session's workflow event must not trigger a read for this one.
harness.emitWorkflowUpdate("session-b");
await new Promise((resolve) => setTimeout(resolve, 20));
expect(harness.loads.length).toBe(2);
});
it("re-reads the plan issue when this session's turn settles", async () => {
// A plan proposed mid-turn announces itself through one workflow event;
// if that single event is dropped, the host-relayed session_settled
// invalidation is the read-repair point.
const harness = createHarness();
render(
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
);
await waitFor(() => expect(harness.loads.length).toBe(1));
harness.loads[0]!.resolve(null);
harness.emitSessionSettled("session-a");
await waitFor(() => expect(harness.loads.length).toBe(2));
harness.loads[1]!.resolve({
kind: "issue",
issue: snapshotFor("session-a")
});
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-a"
)
);
// Another session's settle must not trigger a read for this one.
harness.emitSessionSettled("session-b");
await new Promise((resolve) => setTimeout(resolve, 20));
expect(harness.loads.length).toBe(2);
});
it("loads the new scope when the conversation flips mid-flight", async () => {
const harness = createHarness();
const view = render(
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
);
await waitFor(() => expect(harness.loads.length).toBeGreaterThan(0));
const pendingA = [...harness.loads];
view.rerender(
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-b" />
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
);
// The stale loads resolve late and must be discarded.
for (const load of pendingA) {
load.resolve({ kind: "issue", issue: snapshotFor(load.sessionId) });
}
await waitFor(() =>
expect(
harness.loads.filter((load) => load.sessionId === "session-b").length
).toBeGreaterThan(0)
);
for (const load of harness.loads.filter(
(load) => load.sessionId === "session-b"
)) {
load.resolve({ kind: "issue", issue: snapshotFor(load.sessionId) });
}
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-b"
)
);
});
it("retries via issue updates after an initially empty load", async () => {
const harness = createHarness();
render(
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
);
await waitFor(() => expect(harness.loads.length).toBeGreaterThan(0));
for (const load of [...harness.loads]) {
load.resolve(null);
}
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent("none")
);
const before = harness.loads.length;
harness.emitIssueUpdate("tutti-mode-plan-session-a");
await waitFor(() => expect(harness.loads.length).toBeGreaterThan(before));
for (const load of harness.loads.slice(before)) {
load.resolve({ kind: "issue", issue: snapshotFor(load.sessionId) });
}
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-a"
)
);
});
});
@@ -0,0 +1,434 @@
import { act, renderHook } from "@testing-library/react";
import { StrictMode, type ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import {
TuttiModePlanReviewRuntimeProvider,
type TuttiModePlanReviewRuntime
} from "../workspaceWorkflowRuntime";
import type { TuttiModePlanReviewSnapshot } from "./tuttiModePlanPanelProjection";
import { useTuttiModePlanPanels } from "./useTuttiModePlanPanels";
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason: unknown) => void;
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, reject, resolve };
}
function snapshot(sourceSessionId: string): TuttiModePlanReviewSnapshot {
const suffix = sourceSessionId.replace("session-", "");
const workflowId = `workflow-${suffix}`;
const revisionId = `revision-${suffix}`;
return {
workflow: {
id: workflowId,
workspaceId: "workspace-1",
type: "tutti_mode_plan",
owner: "tutti",
triggerKind: "agent_cli",
sourceSessionId,
status: "pending_review",
currentRevisionId: revisionId
},
revisions: [
{
id: revisionId,
workflowId,
sequence: 1,
schemaVersion: "tutti-mode-plan/v1",
documentPath: `tutti-mode-plans/${workflowId}/revisions/${"a".repeat(64)}.md`,
sha256: "a".repeat(64),
createdAtUnixMs: 100,
document: {
schema: "tutti-mode-plan/v1",
phase: "task_graph",
title: `Plan ${suffix}`,
topicId: "topic-1",
markdownBody: "## Goal\n\nShip safely",
execution: {
mode: "sequential",
reasoningIntensity: 50,
orchestrationIntensity: 50
},
budget: {
mode: "auto",
tokenLimit: 80_000,
quotaWaterlinePercent: 10
},
tasks: [
{
id: `task-${suffix}`,
title: "Implement",
content: "",
priority: "medium",
agentTargetId: "codex",
dependsOn: []
}
]
}
}
],
checkpoints: [
{
id: `checkpoint-${suffix}`,
workflowId,
kind: "task_review",
revisionId,
status: "pending",
createdAtUnixMs: 110,
updatedAtUnixMs: 110
}
]
};
}
function renderPanels(runtime: TuttiModePlanReviewRuntime) {
return renderHook(
({ sourceSessionId }: { sourceSessionId: string | null }) =>
useTuttiModePlanPanels({
decidedBy: "user-1",
sourceSessionId,
workspaceId: "workspace-1"
}),
{
initialProps: { sourceSessionId: "session-a" as string | null },
wrapper: ({ children }: { children: ReactNode }) => (
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={runtime}>
{children}
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
)
}
);
}
describe("useTuttiModePlanPanels", () => {
it("keeps disabled preview scopes transport-free", async () => {
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn(() => Promise.resolve([snapshot("session-a")])),
decide: vi.fn(),
subscribe: vi.fn(() => () => undefined)
};
const rendered = renderHook(
() =>
useTuttiModePlanPanels({
decidedBy: "user-1",
enabled: false,
sourceSessionId: "session-a",
workspaceId: "workspace-1"
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={runtime}>
{children}
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
)
}
);
await act(async () => undefined);
expect(runtime.listPending).not.toHaveBeenCalled();
expect(runtime.subscribe).not.toHaveBeenCalled();
expect(rendered.result.current.panels).toEqual([]);
});
it("clears session A panels immediately when switching to session B", async () => {
const sessionB = deferred<readonly TuttiModePlanReviewSnapshot[]>();
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn(({ sourceSessionId }) =>
sourceSessionId === "session-a"
? Promise.resolve([snapshot("session-a")])
: sessionB.promise
),
decide: vi.fn(),
subscribe: vi.fn(() => () => undefined)
};
const rendered = renderPanels(runtime);
await act(async () => undefined);
expect(rendered.result.current.panels[0]?.sourceSessionId).toBe(
"session-a"
);
rendered.rerender({ sourceSessionId: "session-b" });
expect(rendered.result.current.panels).toEqual([]);
});
it("ignores session A results across an empty scope before session B", async () => {
const sessionA = deferred<readonly TuttiModePlanReviewSnapshot[]>();
const sessionB = deferred<readonly TuttiModePlanReviewSnapshot[]>();
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn(({ sourceSessionId }) =>
sourceSessionId === "session-a" ? sessionA.promise : sessionB.promise
),
decide: vi.fn(),
subscribe: vi.fn(() => () => undefined)
};
const rendered = renderPanels(runtime);
rendered.rerender({ sourceSessionId: null });
rendered.rerender({ sourceSessionId: "session-b" });
await act(async () => sessionA.resolve([snapshot("session-a")]));
expect(rendered.result.current.panels).toEqual([]);
await act(async () => sessionB.resolve([snapshot("session-b")]));
expect(rendered.result.current.panels[0]?.sourceSessionId).toBe(
"session-b"
);
});
it("does not let an in-flight session A decision mutate session B state", async () => {
const decision = deferred<void>();
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn(({ sourceSessionId }) =>
Promise.resolve([snapshot(sourceSessionId)])
),
decide: vi.fn(() => decision.promise),
subscribe: vi.fn(() => () => undefined)
};
const rendered = renderPanels(runtime);
await act(async () => undefined);
let decisionPromise!: Promise<void>;
act(() => {
decisionPromise = rendered.result.current.decide({
checkpointId: "checkpoint-a",
decision: "accepted",
workflowId: "workflow-a"
});
});
expect(rendered.result.current.submittingCheckpointId).toBe("checkpoint-a");
rendered.rerender({ sourceSessionId: "session-b" });
await act(async () => undefined);
expect(rendered.result.current.panels[0]?.sourceSessionId).toBe(
"session-b"
);
expect(rendered.result.current.submittingCheckpointId).toBeNull();
await act(async () => decision.resolve());
await decisionPromise;
expect(rendered.result.current.panels[0]?.sourceSessionId).toBe(
"session-b"
);
expect(rendered.result.current.submittingCheckpointId).toBeNull();
});
it("reconciles the active scope after reconnect and ignores the older in-flight result", async () => {
const beforeReconnect = deferred<readonly TuttiModePlanReviewSnapshot[]>();
const afterReconnect = deferred<readonly TuttiModePlanReviewSnapshot[]>();
let reconnected = false;
let invalidationListener:
| Parameters<TuttiModePlanReviewRuntime["subscribe"]>[1]
| undefined;
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn(() =>
reconnected ? afterReconnect.promise : beforeReconnect.promise
),
decide: vi.fn(),
subscribe: vi.fn((_workspaceId, listener) => {
invalidationListener = listener;
return () => undefined;
})
};
const rendered = renderPanels(runtime);
await act(async () => undefined);
reconnected = true;
act(() => {
invalidationListener?.({
kind: "connection_restored",
workspaceId: "workspace-1"
});
});
await act(async () => beforeReconnect.resolve([snapshot("session-a")]));
expect(rendered.result.current.panels).toEqual([]);
await act(async () => afterReconnect.resolve([snapshot("session-a")]));
expect(rendered.result.current.panels[0]?.workflowId).toBe("workflow-a");
expect(runtime.listPending).toHaveBeenCalledWith({
sourceSessionId: "session-a",
workspaceId: "workspace-1"
});
});
it("keeps the assignment catalog across a revision_created refresh", async () => {
let invalidationListener:
| Parameters<TuttiModePlanReviewRuntime["subscribe"]>[1]
| undefined;
const listAgents = vi.fn(() =>
Promise.resolve([{ agentTargetId: "codex", label: "Codex" }])
);
const loadAgentOptions = vi.fn(() =>
Promise.resolve({
models: [{ value: "gpt-5.4", label: "GPT-5.4" }],
modelPlans: [],
permissionModes: [{ id: "auto", label: "Auto" }],
reasoningEfforts: [{ value: "high", label: "High" }]
})
);
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn(() => Promise.resolve([snapshot("session-a")])),
decide: vi.fn(),
subscribe: vi.fn((_workspaceId, listener) => {
invalidationListener = listener;
return () => undefined;
}),
assignmentOptions: { listAgents, loadAgentOptions }
};
const rendered = renderPanels(runtime);
await act(async () => undefined);
expect(rendered.result.current.assignmentCatalog.agents).toEqual([
{ agentTargetId: "codex", label: "Codex" }
]);
expect(
rendered.result.current.assignmentCatalog.optionsByAgentId["codex"]
?.models
).toEqual([{ value: "gpt-5.4", label: "GPT-5.4" }]);
// "Request changes" -> agent revises -> revision_created invalidation.
await act(async () => {
invalidationListener?.({
kind: "workflow_updated",
workspaceId: "workspace-1",
workflowId: "workflow-a",
sourceSessionId: "session-a",
checkpointId: "checkpoint-a2",
changeKind: "revision_created"
});
});
await act(async () => undefined);
// The new revision panel must keep a usable catalog: agents stay loaded
// and the directory is not refetched redundantly.
expect(rendered.result.current.assignmentCatalog.agents).toEqual([
{ agentTargetId: "codex", label: "Codex" }
]);
expect(listAgents).toHaveBeenCalledTimes(1);
expect(loadAgentOptions).toHaveBeenCalledTimes(1);
});
it("keeps stale assignment options visible while an invalidated target refreshes", async () => {
let invalidationListener:
| Parameters<TuttiModePlanReviewRuntime["subscribe"]>[1]
| undefined;
const refreshed = deferred<{
models: { value: string; label: string }[];
modelPlans: never[];
permissionModes: { id: string; label: string }[];
reasoningEfforts: { value: string; label: string }[];
}>();
const stale = {
models: [{ value: "gpt-5.4", label: "GPT-5.4" }],
modelPlans: [],
permissionModes: [{ id: "auto", label: "Auto" }],
reasoningEfforts: [{ value: "high", label: "High" }]
};
const loadAgentOptions = vi
.fn()
.mockResolvedValueOnce(stale)
.mockImplementationOnce(() => refreshed.promise);
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn(() => Promise.resolve([snapshot("session-a")])),
decide: vi.fn(),
subscribe: vi.fn((_workspaceId, listener) => {
invalidationListener = listener;
return () => undefined;
}),
assignmentOptions: {
readAgentOptions: vi.fn(() => stale),
listAgents: vi.fn(() =>
Promise.resolve([{ agentTargetId: "codex", label: "Codex" }])
),
loadAgentOptions
}
};
const rendered = renderPanels(runtime);
await act(async () => undefined);
expect(
rendered.result.current.assignmentCatalog.optionsByAgentId["codex"]
?.models
).toEqual([{ value: "gpt-5.4", label: "GPT-5.4" }]);
act(() => {
invalidationListener?.({
kind: "assignment_options_invalidated",
workspaceId: "workspace-1",
agentTargetIds: ["codex"]
});
});
expect(loadAgentOptions).toHaveBeenCalledTimes(2);
expect(
rendered.result.current.assignmentCatalog.optionsByAgentId["codex"]
?.models
).toEqual([{ value: "gpt-5.4", label: "GPT-5.4" }]);
await act(async () =>
refreshed.resolve({
models: [{ value: "gpt-5.5", label: "GPT-5.5" }],
modelPlans: [],
permissionModes: [{ id: "auto", label: "Auto" }],
reasoningEfforts: [{ value: "xhigh", label: "Extra high" }]
})
);
expect(
rendered.result.current.assignmentCatalog.optionsByAgentId["codex"]
?.models
).toEqual([{ value: "gpt-5.5", label: "GPT-5.5" }]);
});
it("retries a failed assignment directory load on the next refresh", async () => {
let invalidationListener:
| Parameters<TuttiModePlanReviewRuntime["subscribe"]>[1]
| undefined;
const listAgents = vi
.fn<() => Promise<readonly { agentTargetId: string; label: string }[]>>()
.mockRejectedValueOnce(new Error("directory unavailable"))
.mockResolvedValue([{ agentTargetId: "codex", label: "Codex" }]);
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn(() => Promise.resolve([snapshot("session-a")])),
decide: vi.fn(),
subscribe: vi.fn((_workspaceId, listener) => {
invalidationListener = listener;
return () => undefined;
}),
assignmentOptions: {
listAgents,
loadAgentOptions: vi.fn(() =>
Promise.resolve({
models: [],
modelPlans: [],
permissionModes: [],
reasoningEfforts: []
})
)
}
};
const rendered = renderPanels(runtime);
await act(async () => undefined);
expect(rendered.result.current.assignmentCatalog.agents).toBeNull();
await act(async () => {
invalidationListener?.({
kind: "workflow_updated",
workspaceId: "workspace-1",
workflowId: "workflow-a",
sourceSessionId: "session-a",
checkpointId: "checkpoint-a2",
changeKind: "revision_created"
});
});
await act(async () => undefined);
expect(rendered.result.current.assignmentCatalog.agents).toEqual([
{ agentTargetId: "codex", label: "Codex" }
]);
});
});
@@ -0,0 +1,585 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useOptionalTuttiModePlanReviewRuntime } from "../workspaceWorkflowRuntime";
import type {
TuttiModePlanAssignmentAgentDetail,
TuttiModePlanAssignmentAgentOption,
TuttiModePlanTaskAssignmentInput
} from "../workspaceWorkflowRuntime";
import type {
TuttiPlanIssueMaterializationFailure,
TuttiPlanIssueQueryResult,
TuttiPlanIssueSnapshot
} from "../workspaceWorkflowRuntime";
import {
projectTuttiModePlanPanel,
type TuttiModePlanPanelViewModel,
type TuttiModePlanReviewSnapshot
} from "./tuttiModePlanPanelProjection";
interface PanelState {
error: unknown;
loading: boolean;
scopeKey: string;
snapshots: readonly TuttiModePlanReviewSnapshot[];
submittingCheckpointId: string | null;
}
interface PlanIssueState {
result: TuttiPlanIssueQueryResult;
scopeKey: string;
}
interface AssignmentCatalogState {
agents: readonly TuttiModePlanAssignmentAgentOption[] | null;
optionsByAgentId: Readonly<
Record<string, TuttiModePlanAssignmentAgentDetail>
>;
scopeKey: string;
}
export interface TuttiModePlanAssignmentCatalog {
/** Null until loaded; empty array is a real "no agents" answer. */
agents: readonly TuttiModePlanAssignmentAgentOption[] | null;
optionsByAgentId: Readonly<
Record<string, TuttiModePlanAssignmentAgentDetail>
>;
loadAgentOptions(agentTargetId: string): void;
}
function emptyState(scopeKey: string): PanelState {
return {
error: null,
loading: false,
scopeKey,
snapshots: [],
submittingCheckpointId: null
};
}
function emptyAssignmentState(scopeKey: string): AssignmentCatalogState {
return { agents: null, optionsByAgentId: {}, scopeKey };
}
const EMPTY_AGENT_DETAIL: TuttiModePlanAssignmentAgentDetail = {
models: [],
modelPlans: [],
permissionModes: [],
reasoningEfforts: []
};
const TERMINAL_PLAN_ISSUE_TASK_STATUSES = new Set([
"completed",
"failed",
"canceled"
]);
export function tuttiPlanIssueExecutionIsActive(
issue: TuttiPlanIssueSnapshot | null
): boolean {
return Boolean(
issue &&
!issue.dispatchPaused &&
issue.tasks.some(
(task) =>
!TERMINAL_PLAN_ISSUE_TASK_STATUSES.has(task.status.trim().toLowerCase())
)
);
}
export function useTuttiModePlanPanels(input: {
decidedBy: string;
/** Passive previews keep the hook mounted without starting transport work. */
enabled?: boolean;
sourceSessionId: string | null;
workspaceId: string;
}): {
assignmentCatalog: TuttiModePlanAssignmentCatalog;
decide(input: {
checkpointId: string;
decision: "accepted" | "rejected" | "canceled";
reason?: string | null;
taskAssignments?: readonly TuttiModePlanTaskAssignmentInput[];
workflowId: string;
}): Promise<void>;
error: unknown;
loading: boolean;
panels: readonly TuttiModePlanPanelViewModel[];
/** Live snapshot of the Issue this session's accepted plan materialized. */
planIssue: TuttiPlanIssueSnapshot | null;
/** The accepted plan's durable create_issue failure, when no Issue exists. */
planIssueMaterializationFailure: TuttiPlanIssueMaterializationFailure | null;
/** Stop the plan issue's execution (pause + cancel runs); null until loaded. */
cancelPlanIssueExecution: (() => Promise<void>) | null;
/** Resolve a task's delegate session for jump-to-conversation; null until loaded. */
resolvePlanIssueTaskSession:
| ((taskId: string) => Promise<{ agentSessionId: string } | null>)
| null;
retry(): void;
submittingCheckpointId: string | null;
} {
const runtime = useOptionalTuttiModePlanReviewRuntime();
const [state, setState] = useState<PanelState>(() => emptyState(""));
const [assignmentState, setAssignmentState] =
useState<AssignmentCatalogState>(() => emptyAssignmentState(""));
const [planIssueState, setPlanIssueState] = useState<PlanIssueState>({
result: null,
scopeKey: ""
});
const requestSequenceRef = useRef(0);
const [retrySequence, setRetrySequence] = useState(0);
const enabled = input.enabled ?? true;
const workspaceId = input.workspaceId.trim();
const sourceSessionId = input.sourceSessionId?.trim() ?? "";
const scopeKey =
enabled && runtime && workspaceId && sourceSessionId
? JSON.stringify([workspaceId, sourceSessionId])
: "";
const activeScopeRef = useRef("");
const assignmentRequestsRef = useRef(new Set<string>());
const planIssueRefreshRef = useRef<(() => void) | null>(null);
const assignmentSource = runtime?.assignmentOptions ?? null;
const loadAgentOptions = useCallback(
(agentTargetId: string): void => {
const capturedScope = scopeKey;
const trimmed = agentTargetId.trim();
if (
!capturedScope ||
!assignmentSource ||
!trimmed ||
assignmentRequestsRef.current.has(trimmed)
) {
return;
}
assignmentRequestsRef.current.add(trimmed);
const cached = assignmentSource.readAgentOptions?.({
workspaceId,
agentTargetId: trimmed
});
if (cached) {
setAssignmentState((current) =>
current.scopeKey === capturedScope
? {
...current,
optionsByAgentId: {
...current.optionsByAgentId,
[trimmed]: cached
}
}
: current
);
}
void assignmentSource
.loadAgentOptions({ workspaceId, agentTargetId: trimmed })
.then((options) => {
if (activeScopeRef.current !== capturedScope) return;
setAssignmentState((current) =>
current.scopeKey === capturedScope
? {
...current,
optionsByAgentId: {
...current.optionsByAgentId,
[trimmed]: options
}
}
: current
);
})
.catch(() => {
// Allow a later explicit agent re-selection to retry the load, but
// settle the entry with an empty catalog so selectors degrade to
// the current values instead of wedging on the loading placeholder.
assignmentRequestsRef.current.delete(trimmed);
if (activeScopeRef.current !== capturedScope) return;
setAssignmentState((current) =>
current.scopeKey === capturedScope &&
current.optionsByAgentId[trimmed] === undefined
? {
...current,
optionsByAgentId: {
...current.optionsByAgentId,
[trimmed]: EMPTY_AGENT_DETAIL
}
}
: current
);
});
},
[assignmentSource, scopeKey, workspaceId]
);
// Catalog loading piggybacks on snapshot refreshes instead of adding a
// component effect: every successful listPending with pending work triggers
// the (deduplicated) agent-directory load plus a preload of the option
// catalogs for agents already referenced by the plan document.
const ensureAssignmentCatalog = useCallback(
(
capturedScope: string,
snapshots: readonly TuttiModePlanReviewSnapshot[]
): void => {
if (!capturedScope || !assignmentSource || snapshots.length === 0) {
return;
}
if (!assignmentRequestsRef.current.has("__agents__")) {
assignmentRequestsRef.current.add("__agents__");
const cached = assignmentSource.readAgents?.({ workspaceId });
if (cached) {
setAssignmentState((current) =>
current.scopeKey === capturedScope
? { ...current, agents: [...cached] }
: current
);
}
void assignmentSource
.listAgents({ workspaceId })
.then((agents) => {
if (activeScopeRef.current !== capturedScope) return;
setAssignmentState((current) =>
current.scopeKey === capturedScope
? { ...current, agents: [...agents] }
: current
);
})
.catch(() => {
assignmentRequestsRef.current.delete("__agents__");
});
}
for (const snapshot of snapshots) {
const revision = snapshot.revisions.find(
(candidate) => candidate.id === snapshot.workflow.currentRevisionId
);
for (const task of revision?.document.tasks ?? []) {
if (task.agentTargetId?.trim()) {
loadAgentOptions(task.agentTargetId);
}
}
}
},
[assignmentSource, loadAgentOptions, workspaceId]
);
const refresh = useCallback(async (): Promise<void> => {
const capturedScope = scopeKey;
if (activeScopeRef.current !== capturedScope) return;
if (!enabled || !runtime || !workspaceId || !sourceSessionId) {
requestSequenceRef.current += 1;
setState(emptyState(capturedScope));
return;
}
const sequence = ++requestSequenceRef.current;
setState((current) =>
current.scopeKey === capturedScope
? { ...current, error: null, loading: true }
: { ...emptyState(capturedScope), loading: true }
);
try {
const snapshots = await runtime.listPending({
workspaceId,
sourceSessionId
});
if (
requestSequenceRef.current !== sequence ||
activeScopeRef.current !== capturedScope
) {
return;
}
setState((current) => ({
...current,
error: null,
loading: false,
scopeKey: capturedScope,
snapshots: [...snapshots]
}));
ensureAssignmentCatalog(capturedScope, snapshots);
} catch (error) {
if (
requestSequenceRef.current !== sequence ||
activeScopeRef.current !== capturedScope
) {
return;
}
setState((current) => ({
...current,
error,
loading: false,
scopeKey: capturedScope,
snapshots: []
}));
}
}, [
enabled,
ensureAssignmentCatalog,
runtime,
scopeKey,
sourceSessionId,
workspaceId
]);
const planIssueSource = runtime?.planIssues ?? null;
useEffect(() => {
requestSequenceRef.current += 1;
activeScopeRef.current = scopeKey;
assignmentRequestsRef.current = new Set<string>();
setAssignmentState(emptyAssignmentState(scopeKey));
setPlanIssueState({ result: null, scopeKey });
void refresh();
const capturedScope = scopeKey;
// The materialized plan Issue shares this scope's lifecycle: load with the
// review snapshots and refresh on workspace issue events, with a trailing
// re-read so the view never settles on a snapshot older than the last one.
let planIssueInFlight = false;
let planIssueTrailing = false;
const refreshPlanIssue = (): void => {
if (!capturedScope || !planIssueSource) return;
if (planIssueInFlight) {
planIssueTrailing = true;
return;
}
planIssueInFlight = true;
void planIssueSource
.getSessionPlanIssue({ workspaceId, sourceSessionId })
.then((result) => {
if (activeScopeRef.current !== capturedScope) return;
setPlanIssueState((current) =>
current.scopeKey === capturedScope
? { result, scopeKey: capturedScope }
: current
);
})
.catch(() => {
// Best-effort read model; a later update event retries.
})
.finally(() => {
planIssueInFlight = false;
if (planIssueTrailing && activeScopeRef.current === capturedScope) {
planIssueTrailing = false;
refreshPlanIssue();
}
});
};
refreshPlanIssue();
planIssueRefreshRef.current = refreshPlanIssue;
const unsubscribe =
enabled && runtime && workspaceId && sourceSessionId
? runtime.subscribe(workspaceId, (update) => {
if (update.kind === "assignment_options_invalidated") {
for (const agentTargetId of update.agentTargetIds) {
const normalizedAgentTargetId = agentTargetId.trim();
if (
normalizedAgentTargetId &&
assignmentRequestsRef.current.delete(normalizedAgentTargetId)
) {
loadAgentOptions(normalizedAgentTargetId);
}
}
return;
}
if (
update.kind === "connection_restored" ||
update.sourceSessionId === sourceSessionId
) {
void refresh();
// The workflow's operation_updated event is the authoritative
// "Issue materialized" signal: workspace.issue.updated fires
// during materialization, before the create_issue operation
// records its outcome, so a re-read triggered only by the issue
// event reliably finds no succeeded operation and leaves the
// panel invisible until a remount.
refreshPlanIssue();
}
})
: undefined;
const unsubscribePlanIssue =
capturedScope && planIssueSource
? planIssueSource.subscribeIssueUpdates(workspaceId, () => {
refreshPlanIssue();
})
: undefined;
return () => {
planIssueRefreshRef.current = null;
unsubscribe?.();
unsubscribePlanIssue?.();
if (activeScopeRef.current === scopeKey) {
activeScopeRef.current = "";
}
requestSequenceRef.current += 1;
};
}, [
enabled,
planIssueSource,
refresh,
retrySequence,
runtime,
scopeKey,
sourceSessionId,
workspaceId
]);
const decide = useCallback(
async (decision: {
checkpointId: string;
decision: "accepted" | "rejected" | "canceled";
reason?: string | null;
taskAssignments?: readonly TuttiModePlanTaskAssignmentInput[];
workflowId: string;
}): Promise<void> => {
const capturedScope = scopeKey;
if (
!runtime ||
!capturedScope ||
activeScopeRef.current !== capturedScope
)
return;
setState((current) => ({
...(current.scopeKey === capturedScope
? current
: emptyState(capturedScope)),
error: null,
submittingCheckpointId: decision.checkpointId,
scopeKey: capturedScope
}));
try {
await runtime.decide({
workspaceId,
workflowId: decision.workflowId,
checkpointId: decision.checkpointId,
decision: decision.decision,
decidedBy: input.decidedBy,
reason: decision.reason,
taskAssignments: decision.taskAssignments
});
if (activeScopeRef.current !== capturedScope) return;
await refresh();
// The decide response returns after the daemon completed the
// create_issue operation, so this read observes the materialized
// Issue immediately — the accepted review panel hands over to the
// issue panel in place without waiting for an event round-trip.
planIssueRefreshRef.current?.();
} catch (error) {
if (activeScopeRef.current === capturedScope) {
setState((current) => ({ ...current, error }));
}
} finally {
if (activeScopeRef.current === capturedScope) {
setState((current) => ({
...current,
submittingCheckpointId: null
}));
}
}
},
[input.decidedBy, refresh, runtime, scopeKey, workspaceId]
);
const visibleState =
state.scopeKey === scopeKey ? state : emptyState(scopeKey);
const panels = useMemo(
() =>
visibleState.snapshots
.map(projectTuttiModePlanPanel)
.filter((panel): panel is TuttiModePlanPanelViewModel => panel !== null)
.sort(
(left, right) =>
left.checkpoint.createdAtUnixMs - right.checkpoint.createdAtUnixMs
),
[visibleState.snapshots]
);
const visiblePlanIssueResult =
planIssueState.scopeKey === scopeKey ? planIssueState.result : null;
const visiblePlanIssue =
visiblePlanIssueResult?.kind === "issue"
? visiblePlanIssueResult.issue
: null;
const visiblePlanIssueFailure = useMemo(
() =>
visiblePlanIssueResult?.kind === "materialization_failed"
? {
workflowId: visiblePlanIssueResult.workflowId,
sourceTurnId: visiblePlanIssueResult.sourceTurnId,
errorMessage: visiblePlanIssueResult.errorMessage
}
: null,
[visiblePlanIssueResult]
);
const planIssueId = visiblePlanIssue?.issueId ?? "";
const cancelPlanIssueExecutionAction =
useCallback(async (): Promise<void> => {
if (!planIssueSource || !planIssueId) return;
// Stop is one daemon-owned cascade: pause dispatch, cancel every running
// run. The issue-updated event refreshes the embed.
await planIssueSource.cancelExecution({
workspaceId,
issueId: planIssueId
});
}, [planIssueId, planIssueSource, workspaceId]);
const resolvePlanIssueTaskSessionAction = useCallback(
(taskId: string): Promise<{ agentSessionId: string } | null> => {
if (!planIssueSource || !planIssueId) return Promise.resolve(null);
return planIssueSource.resolveTaskSession({
workspaceId,
issueId: planIssueId,
taskId
});
},
[planIssueId, planIssueSource, workspaceId]
);
const cancelPlanIssueExecution =
planIssueSource && planIssueId ? cancelPlanIssueExecutionAction : null;
const resolvePlanIssueTaskSession =
planIssueSource && planIssueId ? resolvePlanIssueTaskSessionAction : null;
const visibleAssignmentState =
assignmentState.scopeKey === scopeKey
? assignmentState
: emptyAssignmentState(scopeKey);
const assignmentCatalog = useMemo(
() => ({
agents: assignmentSource ? visibleAssignmentState.agents : null,
optionsByAgentId: visibleAssignmentState.optionsByAgentId,
loadAgentOptions
}),
[
assignmentSource,
loadAgentOptions,
visibleAssignmentState.agents,
visibleAssignmentState.optionsByAgentId
]
);
const retry = useCallback(
() => setRetrySequence((current) => current + 1),
[]
);
return useMemo(
() => ({
assignmentCatalog,
decide,
error: visibleState.error,
loading: visibleState.loading,
panels,
planIssue: visiblePlanIssue,
planIssueMaterializationFailure: visiblePlanIssueFailure,
cancelPlanIssueExecution,
resolvePlanIssueTaskSession,
retry,
submittingCheckpointId: visibleState.submittingCheckpointId
}),
[
assignmentCatalog,
cancelPlanIssueExecution,
decide,
panels,
resolvePlanIssueTaskSession,
retry,
visiblePlanIssue,
visiblePlanIssueFailure,
visibleState.error,
visibleState.loading,
visibleState.submittingCheckpointId
]
);
}
@@ -0,0 +1,228 @@
import { createContext, useContext, type JSX, type ReactNode } from "react";
import type { AgentActivityComposerSettingOption } from "@tutti-os/agent-activity-core";
import type { TuttiModePlanReviewSnapshot } from "./tuttiModePlan/tuttiModePlanPanelProjection";
export interface TuttiModePlanReviewUpdate {
kind: "workflow_updated";
workspaceId: string;
workflowId: string;
sourceSessionId: string;
checkpointId: string;
changeKind:
| "proposal_created"
| "revision_created"
| "checkpoint_decided"
| "operation_updated";
}
export interface TuttiModePlanReviewConnectionRestored {
kind: "connection_restored";
workspaceId: string;
}
/**
* A root turn of the source session settled. The host derives this from the
* daemon's canonical turn fan-out and relays it as a read-repair trigger: a
* plan proposed mid-turn announces itself through one workflow event, and if
* that single event is lost no later workflow signal re-reads review state.
*/
export interface TuttiModePlanReviewSessionSettled {
kind: "session_settled";
workspaceId: string;
sourceSessionId: string;
}
/** Cached assignment catalogs changed for exact Agent Targets. */
export interface TuttiModePlanAssignmentOptionsInvalidated {
kind: "assignment_options_invalidated";
workspaceId: string;
agentTargetIds: readonly string[];
}
export type TuttiModePlanReviewInvalidation =
| TuttiModePlanReviewUpdate
| TuttiModePlanReviewConnectionRestored
| TuttiModePlanReviewSessionSettled
| TuttiModePlanAssignmentOptionsInvalidated;
export interface TuttiModePlanTaskAssignmentInput {
taskId: string;
agentTargetId?: string | null;
modelPlanId?: string | null;
model?: string | null;
permissionModeId?: string | null;
reasoningEffort?: string | null;
parallelizable?: boolean | null;
autoAccept?: boolean | null;
}
export interface TuttiModePlanReviewDecisionInput {
workspaceId: string;
workflowId: string;
checkpointId: string;
decision: "accepted" | "rejected" | "canceled";
decidedBy: string;
reason?: string | null;
/** Per-task overrides; only meaningful with an accepted task review. */
taskAssignments?: readonly TuttiModePlanTaskAssignmentInput[];
}
export interface TuttiModePlanAssignmentAgentOption {
agentTargetId: string;
label: string;
}
export interface TuttiModePlanAssignmentAgentDetail {
/** Provider-native models usable without a model plan. */
models: readonly AgentActivityComposerSettingOption[];
modelPlans: readonly {
modelPlanId: string;
label: string;
models: readonly AgentActivityComposerSettingOption[];
}[];
permissionModes: readonly { id: string; label: string }[];
reasoningEfforts: readonly AgentActivityComposerSettingOption[];
}
export interface TuttiPlanIssueTaskSnapshot {
taskId: string;
title: string;
content: string;
status: string;
sortIndex: number;
parallelizable: boolean;
autoAccept: boolean;
dependencyTaskIds: string[];
}
/** Read-only snapshot of the Issue a session's accepted plan materialized. */
export interface TuttiPlanIssueSnapshot {
workflowId: string;
sourceTurnId: string | null;
issueId: string;
topicId: string;
title: string;
/** Durable Issue execution gate; paused graphs are not active work. */
dispatchPaused: boolean;
tasks: TuttiPlanIssueTaskSnapshot[];
}
/**
* An accepted plan whose create_issue operation durably failed. The
* conversation must surface this instead of rendering nothing: there is no
* pending checkpoint (the review panel is gone) and no Issue (the issue panel
* never appears), so silence would hide the failure entirely.
*/
export interface TuttiPlanIssueMaterializationFailure {
workflowId: string;
sourceTurnId: string | null;
errorMessage: string | null;
}
export type TuttiPlanIssueQueryResult =
| { kind: "issue"; issue: TuttiPlanIssueSnapshot }
| ({ kind: "materialization_failed" } & TuttiPlanIssueMaterializationFailure)
| null;
/**
* Source for the embedded plan-issue panel in the conversation.
* The host resolves "the Issue this session's accepted plan created", relays
* live issue updates, and exposes only daemon-owned product commands. Task
* accept/rework actions remain source-Agent prompts rather than generic Issue
* mutations.
*/
export interface TuttiPlanIssueSource {
getSessionPlanIssue(input: {
workspaceId: string;
sourceSessionId: string;
}): Promise<TuttiPlanIssueQueryResult>;
subscribeIssueUpdates(
workspaceId: string,
listener: (update: { issueId: string }) => void
): () => void;
/**
* Stops the Issue's execution: pauses future dispatch and cancels every
* running task run. Idempotent; the daemon owns the cascade.
*/
cancelExecution(input: {
workspaceId: string;
issueId: string;
}): Promise<void>;
/**
* Resolves the delegate agent session that ran (or is running) a task's
* latest run, so a task card can jump straight into that conversation.
* Null when the task has not launched yet.
*/
resolveTaskSession(input: {
workspaceId: string;
issueId: string;
taskId: string;
}): Promise<{ agentSessionId: string } | null>;
}
/**
* Option catalogs for per-task assignment editing. The desktop host reuses
* its agent directory and composer capability catalogs; the panel never
* hardcodes providers or modes.
*/
export interface TuttiModePlanAssignmentOptionsSource {
/**
* Returns the last successful workspace directory, including stale data.
* Hosts may omit this when they do not retain a query cache.
*/
readAgents?(input: {
workspaceId: string;
}): readonly TuttiModePlanAssignmentAgentOption[] | null;
listAgents(input: {
workspaceId: string;
}): Promise<readonly TuttiModePlanAssignmentAgentOption[]>;
/**
* Returns the last successful target catalog, including stale data, so a
* refresh never replaces usable selectors with a loading placeholder.
*/
readAgentOptions?(input: {
workspaceId: string;
agentTargetId: string;
}): TuttiModePlanAssignmentAgentDetail | null;
loadAgentOptions(input: {
workspaceId: string;
agentTargetId: string;
}): Promise<TuttiModePlanAssignmentAgentDetail>;
}
export interface TuttiModePlanReviewRuntime {
listPending(input: {
workspaceId: string;
sourceSessionId: string;
}): Promise<readonly TuttiModePlanReviewSnapshot[]>;
decide(input: TuttiModePlanReviewDecisionInput): Promise<void>;
subscribe(
workspaceId: string,
listener: (update: TuttiModePlanReviewInvalidation) => void
): () => void;
/** Optional; the panel degrades to read-only assignment display without it. */
assignmentOptions?: TuttiModePlanAssignmentOptionsSource;
/** Optional; without it the embedded plan-issue panel never renders. */
planIssues?: TuttiPlanIssueSource;
}
const TuttiModePlanReviewRuntimeContext =
createContext<TuttiModePlanReviewRuntime | null>(null);
export function TuttiModePlanReviewRuntimeProvider({
children,
runtime
}: {
children: ReactNode;
runtime?: TuttiModePlanReviewRuntime | null;
}): JSX.Element {
return (
<TuttiModePlanReviewRuntimeContext.Provider value={runtime ?? null}>
{children}
</TuttiModePlanReviewRuntimeContext.Provider>
);
}
export function useOptionalTuttiModePlanReviewRuntime(): TuttiModePlanReviewRuntime | null {
return useContext(TuttiModePlanReviewRuntimeContext);
}