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
586 lines
19 KiB
TypeScript
586 lines
19 KiB
TypeScript
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
|
|
]
|
|
);
|
|
}
|