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

187 lines
6.2 KiB
TypeScript

import {
selectFailedNewActivationResolution,
selectEngineSessionReconcile,
selectWorkspaceAgentConsumerSession,
type AgentSessionEngine
} from "@tutti-os/agent-activity-core";
import type { Dispatch, RefObject, SetStateAction } from "react";
import { useCallback, useEffect } from "react";
import type { AgentGUIConversationSummary } from "../model/agentGuiConversationModel";
import {
normalizeAgentGUIOpenSessionRequest,
type AgentGUIOpenSessionRequest
} from "./agentGuiController.draftMessageHelpers";
import {
resolveConversationSummaryById,
type AgentGUIConversationSelectionOptions,
type ConversationIntent
} from "./useAgentConversationSelection";
interface UseAgentGUIConversationRoutingInput {
activeConversationIdRef: RefObject<string | null>;
conversationListQuery: unknown | null;
conversations: readonly AgentGUIConversationSummary[];
conversationsRef: RefObject<AgentGUIConversationSummary[]>;
handledOpenSessionSequenceRef: RefObject<number | null>;
hasLoadedConversations: boolean;
intent: ConversationIntent;
openSessionRequest: AgentGUIOpenSessionRequest | null | undefined;
pendingOpenSessionRequestRef: RefObject<AgentGUIOpenSessionRequest | null>;
selectConversation(
agentSessionId: string,
options?: AgentGUIConversationSelectionOptions
): void;
sessionEngine: AgentSessionEngine;
setIntent: Dispatch<SetStateAction<ConversationIntent>>;
transientConversation: AgentGUIConversationSummary | null;
workspaceId: string;
}
export function useAgentGUIConversationRouting(
input: UseAgentGUIConversationRoutingInput
): void {
const {
activeConversationIdRef,
conversationListQuery,
conversations,
conversationsRef,
handledOpenSessionSequenceRef,
hasLoadedConversations,
intent,
openSessionRequest,
pendingOpenSessionRequestRef,
selectConversation,
sessionEngine,
setIntent,
transientConversation,
workspaceId
} = input;
const ensureTransientOpenSessionConversation = useCallback(
(agentSessionId: string) => {
const normalizedAgentSessionId = agentSessionId.trim();
if (!normalizedAgentSessionId) return;
if (
resolveConversationSummaryById(
conversationsRef.current,
normalizedAgentSessionId,
transientConversation
)
) {
return;
}
const consumerSession = selectWorkspaceAgentConsumerSession(
sessionEngine.getSnapshot(),
normalizedAgentSessionId
);
if (consumerSession && consumerSession.session.visible !== false) return;
const reconcile = selectEngineSessionReconcile(
sessionEngine.getSnapshot(),
normalizedAgentSessionId
);
if (
reconcile?.inFlightCommandId ||
reconcile?.pendingMessages ||
reconcile?.pendingState
) {
return;
}
sessionEngine.dispatch({
agentSessionId: normalizedAgentSessionId,
needsMessages: true,
needsState: true,
type: "session/reconcileRequested",
workspaceId
});
},
[sessionEngine, transientConversation, workspaceId]
);
useEffect(() => {
const normalizedOpenSessionRequest =
normalizeAgentGUIOpenSessionRequest(openSessionRequest);
if (
normalizedOpenSessionRequest &&
handledOpenSessionSequenceRef.current !==
normalizedOpenSessionRequest.sequence
) {
handledOpenSessionSequenceRef.current =
normalizedOpenSessionRequest.sequence;
pendingOpenSessionRequestRef.current = normalizedOpenSessionRequest;
}
const pendingOpenSessionRequest = pendingOpenSessionRequestRef.current;
const hasExplicitOpenSessionRequest = Boolean(
pendingOpenSessionRequest?.agentSessionId.trim()
);
const resolveId = (id: string) =>
resolveConversationSummaryById(
conversations,
id,
transientConversation
) !== null;
const resolveCanonicalId = (id: string) =>
resolveConversationSummaryById(conversations, id, null) !== null;
if (hasExplicitOpenSessionRequest) {
const requestedId = pendingOpenSessionRequest!.agentSessionId.trim();
if (!hasLoadedConversations) return;
pendingOpenSessionRequestRef.current = null;
selectConversation(requestedId, {
reloadConversations: false,
reveal: "external-open"
});
ensureTransientOpenSessionConversation(requestedId);
return;
}
if (intent.tag !== "home") {
if (
selectFailedNewActivationResolution(
sessionEngine.getSnapshot(),
intent.id,
intent.tag === "active"
? { selectionSource: intent.source }
: undefined
) === "rollback"
) {
// Terminal activation settlement clears the optimistic selection in an
// earlier effect. Do not let this effect's stale active/requested intent
// select the provisional session again during the same effect flush.
setIntent({ tag: "home" });
return;
}
}
switch (intent.tag) {
case "home":
return;
case "active":
if (resolveCanonicalId(intent.id)) return;
if (resolveId(intent.id)) return;
// An active intent is produced by an explicit user/session selection.
// Rail pages are bounded and may not contain that selected session yet;
// list absence must not demote it into the requested/fallback flow.
if (activeConversationIdRef.current === intent.id) return;
if (!hasLoadedConversations) return;
setIntent({ tag: "requested", id: intent.id });
return;
case "requested":
if (!hasLoadedConversations) return;
// Persisted/external selection is authoritative. The bounded list may
// not contain it after restart, so activate it and reconcile detail
// instead of replacing it with the first visible rail row.
selectConversation(intent.id, { reloadConversations: false });
ensureTransientOpenSessionConversation(intent.id);
return;
}
}, [
conversationListQuery,
conversations,
ensureTransientOpenSessionConversation,
hasLoadedConversations,
intent,
openSessionRequest,
selectConversation,
transientConversation
]);
}