feat: implement PI core chat timeline
This commit is contained in:
212
src/stores/coding-workspace.ts
Normal file
212
src/stores/coding-workspace.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useStore } from 'zustand';
|
||||
import { createStore, type StoreApi } from 'zustand/vanilla';
|
||||
import {
|
||||
createCodingProjectConversation,
|
||||
getCodingProjectConfig,
|
||||
listCodingProjectConversations,
|
||||
listCodingProjects,
|
||||
type CodingProjectCatalog,
|
||||
} from '@/lib/coding-projects';
|
||||
import type {
|
||||
CodingConversationMetadata,
|
||||
CodingProjectAgent,
|
||||
CodingProjectConfig,
|
||||
CodingProjectSummary,
|
||||
} from '@/types/coding-project';
|
||||
|
||||
interface CodingWorkspaceDependencies {
|
||||
listProjects(): Promise<CodingProjectCatalog>;
|
||||
getConfig(projectId: string): Promise<{
|
||||
project: CodingProjectSummary;
|
||||
config: CodingProjectConfig;
|
||||
}>;
|
||||
listConversations(projectId: string): Promise<CodingConversationMetadata[]>;
|
||||
createConversation(input: {
|
||||
projectId: string;
|
||||
agentId: string;
|
||||
title: string;
|
||||
}): Promise<CodingConversationMetadata>;
|
||||
}
|
||||
|
||||
export interface CodingWorkspaceState {
|
||||
projects: CodingProjectSummary[];
|
||||
activeProjectId: string | null;
|
||||
activeProject: CodingProjectSummary | null;
|
||||
config: CodingProjectConfig | null;
|
||||
conversations: CodingConversationMetadata[];
|
||||
selectedAgentId: string | null;
|
||||
loadState: 'idle' | 'loading' | 'ready' | 'error';
|
||||
error: string | null;
|
||||
creatingAgentIds: Record<string, true>;
|
||||
load(): Promise<void>;
|
||||
selectAgent(agentId: string): void;
|
||||
ensureConversation(agentId: string): Promise<CodingConversationMetadata>;
|
||||
createConversation(agentId: string): Promise<CodingConversationMetadata>;
|
||||
}
|
||||
|
||||
function enabledAgent(config: CodingProjectConfig | null, agentId: string | null): CodingProjectAgent | null {
|
||||
if (!config || !agentId) return null;
|
||||
return config.agents.find((agent) => (
|
||||
agent.id === agentId && agent.enabled && !agent.archivedAt
|
||||
)) ?? null;
|
||||
}
|
||||
|
||||
function firstEnabledAgent(config: CodingProjectConfig): CodingProjectAgent | null {
|
||||
return config.agents.find((agent) => agent.enabled && !agent.archivedAt && agent.pinned)
|
||||
?? config.agents.find((agent) => agent.enabled && !agent.archivedAt)
|
||||
?? null;
|
||||
}
|
||||
|
||||
function newestConversation(
|
||||
conversations: CodingConversationMetadata[],
|
||||
agentId: string,
|
||||
): CodingConversationMetadata | null {
|
||||
return conversations
|
||||
.filter((conversation) => conversation.agentId === agentId && !conversation.archivedAt)
|
||||
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0] ?? null;
|
||||
}
|
||||
|
||||
function defaultDependencies(): CodingWorkspaceDependencies {
|
||||
return {
|
||||
listProjects: listCodingProjects,
|
||||
getConfig: getCodingProjectConfig,
|
||||
listConversations: listCodingProjectConversations,
|
||||
createConversation: createCodingProjectConversation,
|
||||
};
|
||||
}
|
||||
|
||||
export function createCodingWorkspaceStore(
|
||||
dependencies: Partial<CodingWorkspaceDependencies> = {},
|
||||
): StoreApi<CodingWorkspaceState> {
|
||||
const deps = { ...defaultDependencies(), ...dependencies };
|
||||
const conversationFlights = new Map<string, Promise<CodingConversationMetadata>>();
|
||||
let loadFlight: Promise<void> | null = null;
|
||||
let loadGeneration = 0;
|
||||
|
||||
return createStore<CodingWorkspaceState>((set, get) => ({
|
||||
projects: [],
|
||||
activeProjectId: null,
|
||||
activeProject: null,
|
||||
config: null,
|
||||
conversations: [],
|
||||
selectedAgentId: null,
|
||||
loadState: 'idle',
|
||||
error: null,
|
||||
creatingAgentIds: {},
|
||||
|
||||
async load() {
|
||||
if (loadFlight) return await loadFlight;
|
||||
const generation = ++loadGeneration;
|
||||
set({ loadState: 'loading', error: null });
|
||||
let flight: Promise<void>;
|
||||
flight = deps.listProjects()
|
||||
.then(async (catalog) => {
|
||||
if (generation !== loadGeneration) return;
|
||||
const activeProject = catalog.projects.find((project) => (
|
||||
project.id === catalog.activeProjectId
|
||||
)) ?? null;
|
||||
if (!activeProject) {
|
||||
set({
|
||||
projects: catalog.projects,
|
||||
activeProjectId: null,
|
||||
activeProject: null,
|
||||
config: null,
|
||||
conversations: [],
|
||||
selectedAgentId: null,
|
||||
loadState: 'ready',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const [snapshot, conversations] = await Promise.all([
|
||||
deps.getConfig(activeProject.id),
|
||||
deps.listConversations(activeProject.id),
|
||||
]);
|
||||
if (generation !== loadGeneration) return;
|
||||
const currentAgentId = enabledAgent(snapshot.config, get().selectedAgentId)?.id ?? null;
|
||||
set({
|
||||
projects: catalog.projects,
|
||||
activeProjectId: activeProject.id,
|
||||
activeProject: snapshot.project,
|
||||
config: snapshot.config,
|
||||
conversations,
|
||||
selectedAgentId: currentAgentId ?? firstEnabledAgent(snapshot.config)?.id ?? null,
|
||||
loadState: 'ready',
|
||||
error: null,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (generation !== loadGeneration) return;
|
||||
set({
|
||||
loadState: 'error',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
if (loadFlight === flight) loadFlight = null;
|
||||
});
|
||||
loadFlight = flight;
|
||||
await flight;
|
||||
},
|
||||
|
||||
selectAgent(agentId) {
|
||||
if (!enabledAgent(get().config, agentId)) return;
|
||||
set({ selectedAgentId: agentId });
|
||||
},
|
||||
|
||||
async ensureConversation(agentId) {
|
||||
const existing = newestConversation(get().conversations, agentId);
|
||||
if (existing) return existing;
|
||||
return await get().createConversation(agentId);
|
||||
},
|
||||
|
||||
async createConversation(agentId) {
|
||||
const state = get();
|
||||
if (!state.activeProject || !enabledAgent(state.config, agentId)) {
|
||||
throw new Error('当前项目没有可用的伙伴。');
|
||||
}
|
||||
const flightKey = `${state.activeProject.id}:${agentId}`;
|
||||
const existingFlight = conversationFlights.get(flightKey);
|
||||
if (existingFlight) return await existingFlight;
|
||||
set((current) => ({
|
||||
creatingAgentIds: { ...current.creatingAgentIds, [agentId]: true },
|
||||
error: null,
|
||||
}));
|
||||
let flight: Promise<CodingConversationMetadata>;
|
||||
flight = deps.createConversation({
|
||||
projectId: state.activeProject.id,
|
||||
agentId,
|
||||
title: '新对话',
|
||||
}).then((conversation) => {
|
||||
if (get().activeProjectId !== state.activeProject?.id) return conversation;
|
||||
set((current) => ({
|
||||
conversations: [
|
||||
conversation,
|
||||
...current.conversations.filter((item) => item.id !== conversation.id),
|
||||
],
|
||||
}));
|
||||
return conversation;
|
||||
}).catch((error) => {
|
||||
set({ error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
conversationFlights.delete(flightKey);
|
||||
set((current) => {
|
||||
const creatingAgentIds = { ...current.creatingAgentIds };
|
||||
delete creatingAgentIds[agentId];
|
||||
return { creatingAgentIds };
|
||||
});
|
||||
});
|
||||
conversationFlights.set(flightKey, flight);
|
||||
return await flight;
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export const codingWorkspaceStore = createCodingWorkspaceStore();
|
||||
|
||||
export function useCodingWorkspaceStore<T>(
|
||||
selector: (state: CodingWorkspaceState) => T,
|
||||
): T {
|
||||
return useStore(codingWorkspaceStore, selector);
|
||||
}
|
||||
Reference in New Issue
Block a user