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

188 lines
6.3 KiB
TypeScript

import { useCallback, useRef } from "react";
import type { AgentGUINodeData } from "../../../types";
import type { AgentGUIConversationSummary } from "../model/agentGuiConversationModel";
import type { AgentGUIConversationRailRevealReason } from "../model/agentGuiConversationRailViewState";
import { rememberAgentGUIActiveConversation } from "../model/agentGuiSessionNavigationMemory";
export type ConversationIntent =
| { tag: "home" }
| { tag: "requested"; id: string }
| {
tag: "active";
id: string;
source?: "activation" | "user-selection";
};
export interface AgentGUIConversationSelectionOptions {
reloadConversations?: boolean;
reveal?: Extract<AgentGUIConversationRailRevealReason, "external-open">;
}
export function resolveConversationSummaryById(
conversations: readonly AgentGUIConversationSummary[],
conversationId: string | null | undefined,
transientConversation: AgentGUIConversationSummary | null = null
): AgentGUIConversationSummary | null {
const normalized = conversationId?.trim() ?? "";
if (!normalized) return null;
return (
conversations.find((conversation) => conversation.id === normalized) ??
(transientConversation?.id === normalized ? transientConversation : null)
);
}
interface AgentConversationSelectionInput {
activation: {
canReload(agentSessionId: string): boolean;
forget(agentSessionId: string): void;
isPending(agentSessionId: string): boolean;
};
conversations: {
agentTargetIdFor(agentSessionId: string): string | null;
contains(agentSessionId: string): boolean;
};
detail: {
ensureHydrated(agentSessionId: string): void;
ensureStateHydrated(agentSessionId: string): void;
isHydrated(agentSessionId: string): boolean;
isStateHydrated(agentSessionId: string): boolean;
markPending(agentSessionId: string): void;
setLoading(loading: boolean): void;
};
hasConversationListQuery(): boolean;
isMounted(): boolean;
onMissingConversationListQuery(previousAgentSessionId: string | null): void;
persistence: {
update(updater: (current: AgentGUINodeData) => AgentGUINodeData): void;
};
rail: {
clearRevealRequest(): void;
requestReveal(
agentSessionId: string,
reason: AgentGUIConversationRailRevealReason
): void;
};
selection: {
clearDetailError(): void;
getActiveSessionId(): string | null;
setActiveSessionId(agentSessionId: string | null): void;
setComposerHome(home: boolean): void;
setIntent(intent: ConversationIntent): void;
};
}
export function useAgentConversationSelection(
input: AgentConversationSelectionInput
) {
const inputRef = useRef(input);
inputRef.current = input;
const persistActiveConversation = useCallback(
(agentSessionId: string | null, agentTargetId?: string | null) => {
inputRef.current.persistence.update((current) => {
const resolvedAgentTargetId = agentSessionId
? agentTargetId?.trim() ||
inputRef.current.conversations.agentTargetIdFor(agentSessionId)
: null;
return rememberAgentGUIActiveConversation(
current,
agentSessionId,
resolvedAgentTargetId
);
});
},
[]
);
const syncConversationListProjection = useCallback(
async (_preferredSessionId?: string | null) => {
const current = inputRef.current;
if (current.hasConversationListQuery()) return;
const previous = current.selection.getActiveSessionId();
current.onMissingConversationListQuery(previous);
current.selection.setIntent({ tag: "home" });
current.selection.setComposerHome(true);
current.selection.setActiveSessionId(null);
current.detail.setLoading(false);
current.selection.clearDetailError();
current.rail.clearRevealRequest();
persistActiveConversation(null);
},
[persistActiveConversation]
);
const selectConversation = useCallback(
(
agentSessionId: string,
options?: AgentGUIConversationSelectionOptions
) => {
const normalized = agentSessionId.trim();
if (!normalized) return;
const current = inputRef.current;
const previous = current.selection.getActiveSessionId();
const detailHydrated = current.detail.isHydrated(normalized);
const stateHydrated = current.detail.isStateHydrated(normalized);
const activationPending = current.activation.isPending(normalized);
current.selection.setComposerHome(false);
if (previous && previous !== normalized)
current.activation.forget(previous);
if (previous !== normalized) {
if (detailHydrated || activationPending) {
current.detail.setLoading(false);
} else {
current.detail.markPending(normalized);
}
}
const reloadConversations =
options?.reloadConversations !== false &&
current.conversations.contains(normalized);
current.selection.setIntent({
id: normalized,
source: "user-selection",
tag: "active"
});
current.selection.setActiveSessionId(normalized);
current.selection.clearDetailError();
if (options?.reveal) {
current.rail.requestReveal(normalized, options.reveal);
} else {
current.rail.clearRevealRequest();
}
if (!activationPending && current.activation.canReload(normalized)) {
if (reloadConversations) {
void syncConversationListProjection(normalized);
}
if (!detailHydrated) {
current.detail.ensureHydrated(normalized);
} else if (!stateHydrated) {
current.detail.ensureStateHydrated(normalized);
}
}
persistActiveConversation(normalized);
},
[persistActiveConversation, syncConversationListProjection]
);
const isCurrentConversation = useCallback(
(agentSessionId: string) =>
inputRef.current.isMounted() &&
inputRef.current.selection.getActiveSessionId() === agentSessionId.trim(),
[]
);
const isConversationStale = useCallback(
(agentSessionId: string) => {
const normalized = agentSessionId.trim();
return Boolean(normalized && !isCurrentConversation(normalized));
},
[isCurrentConversation]
);
return {
isCurrentConversation,
isConversationStale,
persistActiveConversation,
selectConversation,
syncConversationListProjection
};
}