feat(coding): add conversation archive and first-message titles
This commit is contained in:
1 parent
d61226532a
commit
fd0293fe3d
22 files changed
+698
-69
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Bot,
|
||||
ChevronRight,
|
||||
@@ -43,6 +44,7 @@ import { AgentBrowserPanel } from './AgentBrowserPanel';
|
||||
import { CodingChangesSummary } from './CodingChangesSummary';
|
||||
import { CodingConversationSidebar } from './CodingConversationSidebar';
|
||||
import { CodingConversationHeader } from './CodingConversationHeader';
|
||||
import { CodingConversationRenameDialog } from './CodingConversationRenameDialog';
|
||||
import { CodingConversationTimeline } from './CodingConversationTimeline';
|
||||
import { CodingInteractionPanel } from './CodingInteractionPanel';
|
||||
import { CodingWelcomeHero } from './CodingWelcomeHero';
|
||||
@@ -170,6 +172,12 @@ export function CodingChatPanel({
|
||||
Record<string, LocalComposerAttachment[]>
|
||||
>({});
|
||||
const [agentBrowserOpen, setAgentBrowserOpen] = useState(false);
|
||||
const [archivedContext, setArchivedContext] = useState<string | null>(null);
|
||||
const [renameTarget, setRenameTarget] = useState<{ projectId: string; conversation: CodingConversationMetadata } | null>(null);
|
||||
const [busyConversationIds, setBusyConversationIds] = useState<Record<string, boolean>>({});
|
||||
const archiveFlights = useRef(new Set<string>());
|
||||
const showArchived = Boolean(activeProject && selectedAgentId
|
||||
&& archivedContext === `${activeProject.id}:${selectedAgentId}`);
|
||||
const appliedNavigationDraftRef = useRef<string | null>(null);
|
||||
const automaticCreationKeyRef = useRef<string | null>(null);
|
||||
const selectedConversationContextRef = useRef<string | null>(null);
|
||||
@@ -185,7 +193,7 @@ export function CodingChatPanel({
|
||||
const selectedConversation = conversations.find((conversation) => (
|
||||
conversation.id === selectedConversationId
|
||||
&& conversation.agentId === selectedAgent?.id
|
||||
&& !conversation.archivedAt
|
||||
&& (!conversation.archivedAt || showArchived)
|
||||
)) ?? null;
|
||||
const targetConversationId = selectedConversation?.id ?? null;
|
||||
const conversationMetadataError = activeProject && targetConversationId
|
||||
@@ -268,6 +276,8 @@ export function CodingChatPanel({
|
||||
|
||||
useEffect(() => {
|
||||
setAgentBrowserOpen(false);
|
||||
setArchivedContext(null);
|
||||
setRenameTarget(null);
|
||||
}, [activeProject?.id]);
|
||||
|
||||
useEffect(() => () => disconnectEvents(), [disconnectEvents]);
|
||||
@@ -309,7 +319,7 @@ export function CodingChatPanel({
|
||||
clearConversationSelection();
|
||||
return;
|
||||
}
|
||||
if (selectedConversation?.agentId === selectedAgent.id && !selectedConversation.archivedAt) {
|
||||
if (selectedConversation?.agentId === selectedAgent.id) {
|
||||
const selectionContext = `${activeProject.id}:${selectedConversation.id}`;
|
||||
if (selectedConversationContextRef.current !== selectionContext) {
|
||||
void selectProjectConversation(activeProject.id, selectedConversation.id)
|
||||
@@ -317,6 +327,10 @@ export function CodingChatPanel({
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (showArchived) {
|
||||
clearConversationSelection();
|
||||
return;
|
||||
}
|
||||
const next = newestConversation(conversations, selectedAgent.id);
|
||||
if (next) {
|
||||
markConversationUnread(next.id, false);
|
||||
@@ -325,6 +339,11 @@ export function CodingChatPanel({
|
||||
return;
|
||||
}
|
||||
const creationKey = `${activeProject.id}:${selectedAgent.id}`;
|
||||
if (conversations.some((conversation) => conversation.agentId === selectedAgent.id)) {
|
||||
selectedConversationContextRef.current = null;
|
||||
clearConversationSelection();
|
||||
return;
|
||||
}
|
||||
if (automaticCreationKeyRef.current === creationKey) return;
|
||||
automaticCreationKeyRef.current = creationKey;
|
||||
void ensureConversation(selectedAgent.id)
|
||||
@@ -349,6 +368,7 @@ export function CodingChatPanel({
|
||||
selectProjectConversation,
|
||||
selectedAgent,
|
||||
selectedConversation,
|
||||
showArchived,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -407,6 +427,7 @@ export function CodingChatPanel({
|
||||
primeConversation(createLocalConversationSnapshot(projectId, conversation));
|
||||
const current = codingWorkspaceStore.getState();
|
||||
if (current.activeProjectId === projectId && current.selectedAgentId === agentId) {
|
||||
setArchivedContext(null);
|
||||
await selectProjectConversation(projectId, conversation.id).catch(() => undefined);
|
||||
}
|
||||
}, [
|
||||
@@ -418,6 +439,40 @@ export function CodingChatPanel({
|
||||
selectedAgent,
|
||||
]);
|
||||
|
||||
const handleArchiveConversation = async (conversation: CodingConversationMetadata, archived = !conversation.archivedAt) => {
|
||||
if (!activeProject || archiveFlights.current.has(conversation.id)) return;
|
||||
const projectId = activeProject.id;
|
||||
const agentId = conversation.agentId;
|
||||
const selectionAtStart = codingConversationStore.getState().selectedConversationId;
|
||||
archiveFlights.current.add(conversation.id);
|
||||
setBusyConversationIds((current) => ({ ...current, [conversation.id]: true }));
|
||||
try {
|
||||
const updated = await patchConversation(conversation.id, { archived });
|
||||
const current = codingWorkspaceStore.getState();
|
||||
if (current.activeProjectId !== projectId || current.selectedAgentId !== agentId) return;
|
||||
if (!archived && codingConversationStore.getState().selectedConversationId === selectionAtStart) {
|
||||
setArchivedContext(null);
|
||||
handleSelectConversation(updated);
|
||||
}
|
||||
if (archived) {
|
||||
const status = codingConversationStore.getState().summariesByConversationId[conversation.id]?.runStatus;
|
||||
const active = status && ['queued', 'running', 'retrying', 'compacting', 'aborting'].includes(status);
|
||||
toast.success(active ? '对话已归档,任务继续运行' : '对话已归档', {
|
||||
action: { label: '撤销', onClick: () => {
|
||||
if (codingWorkspaceStore.getState().activeProjectId === projectId) {
|
||||
void handleArchiveConversation(updated, false);
|
||||
}
|
||||
} },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : '更新对话失败');
|
||||
} finally {
|
||||
archiveFlights.current.delete(conversation.id);
|
||||
setBusyConversationIds((current) => { const next = { ...current }; delete next[conversation.id]; return next; });
|
||||
}
|
||||
};
|
||||
|
||||
const handleForkConversation = useCallback(async (sourceEntryId?: string) => {
|
||||
if (!activeProject || !selectedAgent || !targetConversationId) return;
|
||||
const sourceProjectId = activeProject.id;
|
||||
@@ -704,7 +759,13 @@ export function CodingChatPanel({
|
||||
selectedAgentId={selectedAgent?.id ?? null}
|
||||
selectedConversationId={targetConversationId}
|
||||
creatingAgentIds={creatingAgentIds}
|
||||
showArchived={showArchived}
|
||||
busyConversationIds={busyConversationIds}
|
||||
onToggleArchived={() => setArchivedContext(showArchived ? null : `${activeProject?.id}:${selectedAgentId}`)}
|
||||
onRenameConversation={(conversation) => { if (activeProject) setRenameTarget({ projectId: activeProject.id, conversation }); }}
|
||||
onArchiveConversation={(conversation) => void handleArchiveConversation(conversation)}
|
||||
onSelectAgent={(agentId) => {
|
||||
setArchivedContext(null);
|
||||
clearConversationSelection();
|
||||
selectAgent(agentId);
|
||||
}}
|
||||
@@ -712,6 +773,12 @@ export function CodingChatPanel({
|
||||
onCreateConversation={() => void handleCreateConversation()}
|
||||
onOpenProjectSettings={onOpenProjectSettings}
|
||||
/>
|
||||
{renameTarget && renameTarget.projectId === activeProject?.id && (
|
||||
<CodingConversationRenameDialog key={renameTarget.conversation.id}
|
||||
title={renameTarget.conversation.title}
|
||||
onSave={async (title) => { await patchConversation(renameTarget.conversation.id, { title }); }}
|
||||
onClose={() => setRenameTarget(null)} />
|
||||
)}
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<CodingConversationHeader
|
||||
@@ -758,7 +825,7 @@ export function CodingChatPanel({
|
||||
key={targetConversationId}
|
||||
conversationId={targetConversationId}
|
||||
assistantName={selectedAgent?.name}
|
||||
onFork={(sourceEntryId) => {
|
||||
onFork={selectedConversation?.archivedAt ? undefined : (sourceEntryId) => {
|
||||
void handleForkConversation(sourceEntryId).catch((error) => {
|
||||
if (!draftKey) return;
|
||||
setSubmissionErrors((current) => ({
|
||||
@@ -789,7 +856,11 @@ export function CodingChatPanel({
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="min-h-0 flex-1" data-testid="coding-conversation-pending" />
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground" data-testid="coding-conversation-pending">
|
||||
{conversations.some((conversation) => conversation.agentId === selectedAgentId)
|
||||
? showArchived ? '选择一条已归档会话查看' : '没有最近会话,可新建对话或查看归档'
|
||||
: null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{targetConversationId && (
|
||||
@@ -809,7 +880,13 @@ export function CodingChatPanel({
|
||||
/>
|
||||
)}
|
||||
|
||||
<CodingComposer
|
||||
{selectedConversation?.archivedAt ? (
|
||||
<div className="m-4 flex items-center justify-between gap-3 rounded-xl border bg-surface-subtle px-4 py-3 text-sm">
|
||||
<span>{running ? '此对话已归档,任务仍在运行。' : '此对话已归档,恢复后可继续聊天。'}</span>
|
||||
<Button disabled={busyConversationIds[selectedConversation.id]}
|
||||
onClick={() => void handleArchiveConversation(selectedConversation, false)}>恢复对话</Button>
|
||||
</div>
|
||||
) : <CodingComposer
|
||||
value={draft}
|
||||
editable={editable}
|
||||
canSend={canSend}
|
||||
@@ -866,7 +943,7 @@ export function CodingChatPanel({
|
||||
onRefreshRuntime={async () => {
|
||||
if (targetConversationId) await loadConversationSnapshot(targetConversationId, 'silent');
|
||||
}}
|
||||
/>
|
||||
/>}
|
||||
</div>
|
||||
|
||||
<AgentBrowserPanel
|
||||
|
||||
Reference in new issue
Block a user