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
188 lines
6.3 KiB
TypeScript
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
|
|
};
|
|
}
|