源码快照 @ 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
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:
+401
@@ -0,0 +1,401 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import {
|
||||
selectAttentionReadState,
|
||||
selectRootAgentSessionIdsWithPendingInteractions,
|
||||
selectWorkspaceAgentConsumerSessions
|
||||
} from "@tutti-os/agent-activity-core";
|
||||
import {
|
||||
useAgentGUIRuntime,
|
||||
type AgentGUIRuntime
|
||||
} from "../../../agentActivityRuntime";
|
||||
import {
|
||||
createAgentGUIConversationRailQueryController,
|
||||
type AgentGUIConversationRailQuerySnapshot,
|
||||
type ConversationRailQueryRuntime
|
||||
} from "../../../agentConversationRailController";
|
||||
import { inspectAgentConversationBatchDeletionCapability } from "./agentConversationBatchDeletionCapability";
|
||||
import { useEngineSelector } from "../../../shared/engine/useEngineSelector";
|
||||
import type { AgentGUINodeViewModel } from "../model/agentGuiNodeTypes";
|
||||
import {
|
||||
applyAgentGUIConversationProjects,
|
||||
type AgentGUIConversationSummary
|
||||
} from "../model/agentGuiConversationModel";
|
||||
import type { AgentGUIConversationActivityRootFact } from "../model/agentGuiConversationActivityView";
|
||||
import {
|
||||
filterAgentGUIConversationSummaries,
|
||||
type AgentGUIConversationFilter
|
||||
} from "../model/agentGuiConversationFilter";
|
||||
import { createAgentGUIConversationRailTitlePromptSelector } from "../../../shared/agentConversationRailTitlePromptSelector";
|
||||
import { projectCanonicalAgentGUIConversationSummaries } from "../../../shared/agentGUIConversationSummaryProjection";
|
||||
import { selectRootAgentSessionIdsAwaitingPlanImplementation } from "../../../shared/agentConversation/planImplementationAwaiting";
|
||||
import { conversationSummariesRenderEqual } from "./agentGuiController.stableHelpers";
|
||||
import { createConversationRailConversationsSelector } from "./agentGuiConversationRailQuerySnapshot";
|
||||
import { resolveConversationRailQueryScope } from "./agentGuiConversationRailQueryTypes";
|
||||
import { agentGUIConversationRailViewScopeKey } from "../model/agentGuiConversationRailViewState";
|
||||
import { reportAgentGUIConversationBatchDeletionCapabilityIncomplete } from "./agentGuiController.reporting";
|
||||
|
||||
export interface AgentGUIConversationRailInput {
|
||||
activeConversationId: string | null;
|
||||
activityContextKey?: string;
|
||||
conversationFilter: AgentGUINodeViewModel["rail"]["conversationFilter"];
|
||||
conversationQuery: string;
|
||||
currentUserId?: string | null;
|
||||
nodeId?: string | null;
|
||||
registerInteractionLockProbe?: (probe: (() => boolean) | null) => void;
|
||||
userProjects: AgentGUINodeViewModel["rail"]["userProjects"];
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
const EMPTY_AGENT_GUI_CONVERSATION_ACTIVITY_ROOT_FACTS: ReadonlyMap<
|
||||
string,
|
||||
AgentGUIConversationActivityRootFact
|
||||
> = new Map();
|
||||
const EMPTY_AGENT_GUI_CONVERSATION_ACTIVITY_CONVERSATIONS: readonly AgentGUIConversationSummary[] =
|
||||
[];
|
||||
|
||||
export function useAgentGUIConversationRailQuery({
|
||||
activeConversationId,
|
||||
conversationFilter,
|
||||
conversationQuery,
|
||||
activityContextKey = "",
|
||||
currentUserId,
|
||||
nodeId,
|
||||
registerInteractionLockProbe,
|
||||
userProjects,
|
||||
workspaceId
|
||||
}: AgentGUIConversationRailInput) {
|
||||
const runtime = useAgentGUIRuntime();
|
||||
const batchDeletionCapability = useMemo(
|
||||
() => inspectAgentConversationBatchDeletionCapability(runtime),
|
||||
[runtime]
|
||||
);
|
||||
const engine = useMemo(
|
||||
() => runtime.getSessionEngine(workspaceId),
|
||||
[runtime, workspaceId]
|
||||
);
|
||||
const activityEnabled = runtime.conversationActivityViewEnabled === true;
|
||||
const activeConversationIdRef = useRef(activeConversationId);
|
||||
activeConversationIdRef.current = activeConversationId;
|
||||
const railRuntime = useMemo(
|
||||
() => createAgentGUIConversationRailRuntimeAdapter(runtime, nodeId),
|
||||
[nodeId, runtime]
|
||||
);
|
||||
const controller = useMemo(
|
||||
() =>
|
||||
createAgentGUIConversationRailQueryController({
|
||||
engine,
|
||||
getActiveConversationId: () => activeConversationIdRef.current,
|
||||
runtime: railRuntime,
|
||||
sectionRefreshLimitMax:
|
||||
runtime.conversationRailQueryLimits?.sectionRefreshLimitMax,
|
||||
workspaceId
|
||||
}),
|
||||
[engine, railRuntime, workspaceId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (batchDeletionCapability.partial) {
|
||||
reportAgentGUIConversationBatchDeletionCapabilityIncomplete({
|
||||
missingMethods: batchDeletionCapability.missingMethods,
|
||||
runtime,
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
const detach = controller.attach();
|
||||
registerInteractionLockProbe?.(controller.isInteractionLocked);
|
||||
return () => {
|
||||
registerInteractionLockProbe?.(null);
|
||||
detach();
|
||||
};
|
||||
}, [
|
||||
batchDeletionCapability,
|
||||
controller,
|
||||
registerInteractionLockProbe,
|
||||
runtime,
|
||||
workspaceId
|
||||
]);
|
||||
const querySnapshot = useEngineSelector(
|
||||
controller,
|
||||
identitySnapshot,
|
||||
Object.is
|
||||
);
|
||||
const projectRailConversations = useMemo(() => {
|
||||
const select = createConversationRailConversationsSelector();
|
||||
let previous: ReturnType<typeof select> = [];
|
||||
return (
|
||||
state: Parameters<typeof select>[0]["engineState"],
|
||||
snapshot: AgentGUIConversationRailQuerySnapshot
|
||||
) => {
|
||||
previous = select(
|
||||
{
|
||||
engineState: state,
|
||||
interactionLocked: controller.isInteractionLocked(),
|
||||
querySnapshot: snapshot
|
||||
},
|
||||
previous
|
||||
);
|
||||
return previous;
|
||||
};
|
||||
}, [controller]);
|
||||
const runtimeRailConversations = useEngineSelector(
|
||||
engine,
|
||||
(state) => projectRailConversations(state, querySnapshot),
|
||||
Object.is
|
||||
);
|
||||
const selectActivityRootFacts = useMemo(
|
||||
() =>
|
||||
runtime.conversationActivityViewEnabled === true
|
||||
? selectAgentGUIConversationActivityRootFacts
|
||||
: selectEmptyAgentGUIConversationActivityRootFacts,
|
||||
[runtime.conversationActivityViewEnabled]
|
||||
);
|
||||
const activityRootFacts = useEngineSelector(
|
||||
engine,
|
||||
selectActivityRootFacts,
|
||||
activityRootFactsEqual
|
||||
);
|
||||
const selectActivityTitlePrompts = useMemo(
|
||||
() => createAgentGUIConversationRailTitlePromptSelector(),
|
||||
[engine]
|
||||
);
|
||||
const activityTitlePrompts = useEngineSelector(
|
||||
engine,
|
||||
selectActivityTitlePrompts,
|
||||
Object.is
|
||||
);
|
||||
const selectActivityConversations = useMemo(
|
||||
() => (state: Parameters<typeof selectWorkspaceAgentConsumerSessions>[0]) =>
|
||||
selectCanonicalActivityConversations(state, {
|
||||
activityEnabled,
|
||||
conversationFilter,
|
||||
currentUserId,
|
||||
firstUserDisplayPromptsBySessionId: activityTitlePrompts,
|
||||
userProjects
|
||||
}),
|
||||
[
|
||||
activityEnabled,
|
||||
activityTitlePrompts,
|
||||
conversationFilter,
|
||||
currentUserId,
|
||||
userProjects
|
||||
]
|
||||
);
|
||||
const activityConversations = useEngineSelector(
|
||||
engine,
|
||||
selectActivityConversations,
|
||||
activityConversationArraysEqual
|
||||
);
|
||||
const deletedSessionIds = useEngineSelector(
|
||||
engine,
|
||||
(state) => state.sessionLifecycle.deletedSessionIds
|
||||
);
|
||||
useEffect(() => {
|
||||
controller.configure({
|
||||
conversationFilter,
|
||||
userProjects
|
||||
});
|
||||
controller.setSearchQuery(conversationQuery);
|
||||
controller.activityController.configure({
|
||||
available: activityEnabled,
|
||||
conversations: activityConversations,
|
||||
deletedSessionIds,
|
||||
identityKey: `${workspaceId}\u0000${activityContextKey}`,
|
||||
scopeKey: agentGUIConversationRailViewScopeKey({
|
||||
conversationFilter,
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
}, [
|
||||
activityContextKey,
|
||||
activityConversations,
|
||||
activityEnabled,
|
||||
controller,
|
||||
conversationFilter,
|
||||
conversationQuery,
|
||||
deletedSessionIds,
|
||||
userProjects,
|
||||
workspaceId
|
||||
]);
|
||||
const requestedRailScopeKey = useMemo(
|
||||
() =>
|
||||
resolveConversationRailQueryScope(workspaceId, {
|
||||
conversationFilter,
|
||||
userProjects
|
||||
}).scopeKey,
|
||||
[conversationFilter, userProjects, workspaceId]
|
||||
);
|
||||
const retryRuntimeRail = useCallback(
|
||||
() => controller.refresh(),
|
||||
[controller]
|
||||
);
|
||||
return useMemo(
|
||||
() => ({
|
||||
...querySnapshot,
|
||||
activityController: controller.activityController,
|
||||
batchDeletionAvailable: batchDeletionCapability.available,
|
||||
activityRootFacts,
|
||||
activityConversations,
|
||||
deletedSessionIds,
|
||||
isInteractionLocked: controller.isInteractionLocked,
|
||||
loadMoreSectionConversations: controller.loadMoreSectionConversations,
|
||||
railSearch: {
|
||||
...querySnapshot.railSearch,
|
||||
loadMore: controller.loadMoreSearchResults,
|
||||
retry: controller.retrySearchResults
|
||||
},
|
||||
runtimeRailScopeResolved:
|
||||
!querySnapshot.runtimeSectionsEnabled ||
|
||||
querySnapshot.runtimeRailResolvedScopeKey === requestedRailScopeKey,
|
||||
runtimeRailConversations,
|
||||
retryRuntimeRail
|
||||
}),
|
||||
[
|
||||
batchDeletionCapability.available,
|
||||
activityRootFacts,
|
||||
activityConversations,
|
||||
controller,
|
||||
deletedSessionIds,
|
||||
querySnapshot,
|
||||
requestedRailScopeKey,
|
||||
runtimeRailConversations,
|
||||
retryRuntimeRail
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
function selectEmptyAgentGUIConversationActivityRootFacts(): ReadonlyMap<
|
||||
string,
|
||||
AgentGUIConversationActivityRootFact
|
||||
> {
|
||||
return EMPTY_AGENT_GUI_CONVERSATION_ACTIVITY_ROOT_FACTS;
|
||||
}
|
||||
|
||||
function selectAgentGUIConversationActivityRootFacts(
|
||||
state: Parameters<typeof selectWorkspaceAgentConsumerSessions>[0]
|
||||
): ReadonlyMap<string, AgentGUIConversationActivityRootFact> {
|
||||
const rootSessionIdsAwaitingUserAction = new Set([
|
||||
...selectRootAgentSessionIdsWithPendingInteractions(state),
|
||||
...selectRootAgentSessionIdsAwaitingPlanImplementation(state)
|
||||
]);
|
||||
return new Map(
|
||||
selectWorkspaceAgentConsumerSessions(state)
|
||||
.filter((item) => item.session.visible !== false)
|
||||
.map((item) => [
|
||||
item.session.agentSessionId,
|
||||
{
|
||||
needsUserAction: rootSessionIdsAwaitingUserAction.has(
|
||||
item.session.agentSessionId
|
||||
),
|
||||
status: item.displayStatus === "idle" ? "ready" : item.displayStatus
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function selectCanonicalActivityConversations(
|
||||
state: Parameters<typeof selectWorkspaceAgentConsumerSessions>[0],
|
||||
input: {
|
||||
activityEnabled: boolean;
|
||||
conversationFilter: AgentGUIConversationFilter;
|
||||
currentUserId?: string | null;
|
||||
firstUserDisplayPromptsBySessionId: Record<string, string>;
|
||||
userProjects: AgentGUINodeViewModel["rail"]["userProjects"];
|
||||
}
|
||||
): readonly AgentGUIConversationSummary[] {
|
||||
if (!input.activityEnabled) {
|
||||
return EMPTY_AGENT_GUI_CONVERSATION_ACTIVITY_CONVERSATIONS;
|
||||
}
|
||||
const rootFacts = selectAgentGUIConversationActivityRootFacts(state);
|
||||
const attention = selectAttentionReadState(state, input.currentUserId);
|
||||
const summaries = projectCanonicalAgentGUIConversationSummaries(
|
||||
selectWorkspaceAgentConsumerSessions(state),
|
||||
input.firstUserDisplayPromptsBySessionId
|
||||
).map((conversation): AgentGUIConversationSummary => {
|
||||
const fact = rootFacts.get(conversation.id);
|
||||
const attentionRecord = attention.recordsBySessionId[conversation.id];
|
||||
return {
|
||||
...conversation,
|
||||
hasUnreadCompletion: attentionRecord?.isUnread ?? false,
|
||||
needsUserAction: fact?.needsUserAction ?? conversation.needsUserAction,
|
||||
status: fact?.status ?? conversation.status
|
||||
};
|
||||
});
|
||||
return filterAgentGUIConversationSummaries(
|
||||
applyAgentGUIConversationProjects(summaries, input.userProjects),
|
||||
input.conversationFilter
|
||||
);
|
||||
}
|
||||
|
||||
function activityConversationArraysEqual(
|
||||
left: readonly AgentGUIConversationSummary[],
|
||||
right: readonly AgentGUIConversationSummary[]
|
||||
): boolean {
|
||||
return (
|
||||
left.length === right.length &&
|
||||
left.every((conversation, index) => {
|
||||
const other = right[index];
|
||||
return (
|
||||
other !== undefined &&
|
||||
conversationSummariesRenderEqual(conversation, other)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function activityRootFactsEqual(
|
||||
left: ReadonlyMap<string, AgentGUIConversationActivityRootFact>,
|
||||
right: ReadonlyMap<string, AgentGUIConversationActivityRootFact>
|
||||
): boolean {
|
||||
return (
|
||||
left.size === right.size &&
|
||||
[...left].every(([id, fact]) => {
|
||||
const candidate = right.get(id);
|
||||
return (
|
||||
candidate?.needsUserAction === fact.needsUserAction &&
|
||||
candidate.status === fact.status
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function identitySnapshot(
|
||||
snapshot: AgentGUIConversationRailQuerySnapshot
|
||||
): AgentGUIConversationRailQuerySnapshot {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function createAgentGUIConversationRailRuntimeAdapter(
|
||||
runtime: AgentGUIRuntime,
|
||||
nodeId: string | null | undefined
|
||||
): ConversationRailQueryRuntime {
|
||||
const adapter: ConversationRailQueryRuntime = {};
|
||||
const listPinnedSessionsPage = runtime.listPinnedSessionsPage?.bind(runtime);
|
||||
if (listPinnedSessionsPage) {
|
||||
adapter.listPinnedSessionsPage = listPinnedSessionsPage;
|
||||
}
|
||||
const listSessionSectionPage = runtime.listSessionSectionPage?.bind(runtime);
|
||||
if (listSessionSectionPage) {
|
||||
adapter.listSessionSectionPage = listSessionSectionPage;
|
||||
}
|
||||
const listSessionSections = runtime.listSessionSections?.bind(runtime);
|
||||
if (listSessionSections) {
|
||||
adapter.listSessionSections = listSessionSections;
|
||||
}
|
||||
const listSessionsPage = runtime.listSessionsPage?.bind(runtime);
|
||||
if (listSessionsPage) {
|
||||
adapter.listSessionsPage = listSessionsPage;
|
||||
}
|
||||
const reportDiagnostic = runtime.reportDiagnostic?.bind(runtime);
|
||||
if (reportDiagnostic) {
|
||||
const normalizedNodeId = nodeId?.trim() || null;
|
||||
adapter.reportDiagnostic = (input) =>
|
||||
reportDiagnostic({
|
||||
...input,
|
||||
details: {
|
||||
...input.details,
|
||||
nodeId: normalizedNodeId
|
||||
}
|
||||
});
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
Reference in New Issue
Block a user