feat(coding): add conversation archive and first-message titles

This commit is contained in:
2026-09-20 12:43:51 +08:00
parent d61226532a
commit fd0293fe3d
22 changed files with 698 additions and 69 deletions

View File

@@ -8,6 +8,7 @@ import {
} from '../../shared/coding-conversation-reducer';
import { AppError } from '@/lib/error-model';
import { queueCodingConversationSessionSync } from '@/lib/agent-session-sync';
import { codingWorkspaceStore } from './coding-workspace';
import {
getCodingConversationSnapshot,
openCodingConversationEvents,
@@ -660,8 +661,20 @@ export function createCodingConversationStore(
set({ connectionState: 'error', globalError: 'Conversation 事件更新无法读取。' });
}
});
source.addEventListener('conversation.metadata-changed', (event) => {
if (eventSource !== source) return;
try {
const metadata = parse<{ projectId: string }>(event);
if (typeof metadata.projectId === 'string') {
void codingWorkspaceStore.getState().refreshConversations(metadata.projectId).catch(() => undefined);
}
} catch { /* Reconnection refreshes authoritative metadata. */ }
});
source.onopen = () => {
if (eventSource === source) set({ connectionState: 'live', globalError: null });
if (eventSource !== source) return;
set({ connectionState: 'live', globalError: null });
const workspace = codingWorkspaceStore.getState();
if (workspace.activeProjectId) void workspace.refreshConversations(workspace.activeProjectId).catch(() => undefined);
};
source.onerror = () => {
if (eventSource === source) {

View File

@@ -61,6 +61,7 @@ export interface CodingWorkspaceState {
conversationErrorsByProjectId: Record<string, Record<string, string>>;
creatingAgentIds: Record<string, true>;
load(): Promise<void>;
refreshConversations(projectId: string): Promise<void>;
openProject(projectPath: string): Promise<CodingProjectSummary>;
createProject(input: {
projectPath?: string;
@@ -139,6 +140,9 @@ export function createCodingWorkspaceStore(
const conversationFlights = new Map<string, Promise<CodingConversationMetadata>>();
let loadFlight: Promise<void> | null = null;
let loadGeneration = 0;
let metadataRevision = 0;
let metadataTail = Promise.resolve();
const refreshFlights = new Map<string, { dirty: boolean; promise: Promise<void> }>();
return createStore<CodingWorkspaceState>((set, get) => ({
projects: [],
@@ -152,6 +156,30 @@ export function createCodingWorkspaceStore(
conversationErrorsByProjectId: {},
creatingAgentIds: {},
async refreshConversations(projectId) {
if (get().activeProjectId !== projectId) return;
const existing = refreshFlights.get(projectId);
if (existing) {
existing.dirty = true;
return await existing.promise;
}
const refresh = { dirty: true, promise: Promise.resolve() };
refresh.promise = (async () => {
while (refresh.dirty && get().activeProjectId === projectId) {
refresh.dirty = false;
await metadataTail;
const revision = metadataRevision;
const generation = loadGeneration;
const conversations = await deps.listConversations(projectId);
if (get().activeProjectId !== projectId || generation !== loadGeneration) return;
if (revision !== metadataRevision) { refresh.dirty = true; continue; }
if (!refresh.dirty) set({ conversations });
}
})().finally(() => { refreshFlights.delete(projectId); });
refreshFlights.set(projectId, refresh);
return await refresh.promise;
},
async load() {
if (loadFlight) return await loadFlight;
const generation = ++loadGeneration;
@@ -175,6 +203,8 @@ export function createCodingWorkspaceStore(
});
return;
}
if (get().activeProjectId === activeProject.id) await metadataTail;
const revision = metadataRevision;
const [snapshot, conversations] = await Promise.all([
deps.getConfig(activeProject.id),
deps.listConversations(activeProject.id),
@@ -186,11 +216,13 @@ export function createCodingWorkspaceStore(
activeProjectId: activeProject.id,
activeProject: snapshot.project,
config: snapshot.config,
conversations,
conversations: revision === metadataRevision || get().activeProjectId !== activeProject.id
? conversations : get().conversations,
selectedAgentId: currentAgentId ?? firstEnabledAgent(snapshot.config)?.id ?? null,
loadState: 'ready',
error: null,
});
if (revision !== metadataRevision) void get().refreshConversations(activeProject.id).catch(() => undefined);
})
.catch((error) => {
if (generation !== loadGeneration) return;
@@ -259,6 +291,7 @@ export function createCodingWorkspaceStore(
agentId,
title: '新对话',
}).then((conversation) => {
metadataRevision += 1;
if (get().activeProjectId !== state.activeProject?.id) return conversation;
set((current) => ({
conversations: [
@@ -285,6 +318,9 @@ export function createCodingWorkspaceStore(
async patchConversation(conversationId, patch) {
const sourceProjectId = get().activeProjectId;
if (!sourceProjectId) throw new Error('当前没有可用的项目。');
metadataRevision += 1;
const flight = metadataTail.then(() => deps.patchConversation(conversationId, patch));
metadataTail = flight.then(() => undefined, () => undefined);
set((current) => ({
conversationErrorsByProjectId: withConversationError(
current.conversationErrorsByProjectId,
@@ -294,10 +330,11 @@ export function createCodingWorkspaceStore(
),
}));
try {
const conversation = await deps.patchConversation(conversationId, patch);
const conversation = await flight;
get().upsertConversation(sourceProjectId, conversation);
return conversation;
} catch (error) {
metadataRevision += 1;
const message = error instanceof Error ? error.message : String(error);
set((current) => ({
conversationErrorsByProjectId: withConversationError(
@@ -312,6 +349,7 @@ export function createCodingWorkspaceStore(
},
upsertConversation(projectId, conversation) {
metadataRevision += 1;
if (get().activeProjectId !== projectId) return;
set((current) => ({
conversations: [