fix: close PI core chat review gaps
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { hostApiFetch, hostApiFetchBytes } from './host-api';
|
||||
|
||||
export const CODING_ATTACHMENT_MAX_BYTES = 16 * 1024 * 1024;
|
||||
export const CODING_ATTACHMENT_MAX_COUNT = 16;
|
||||
export const CODING_ATTACHMENT_UPLOAD_CONCURRENCY = 4;
|
||||
export const CODING_ATTACHMENT_MIMES = new Set([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
CODING_ATTACHMENT_MAX_BYTES,
|
||||
CODING_ATTACHMENT_MAX_COUNT,
|
||||
CODING_ATTACHMENT_MIMES,
|
||||
CODING_ATTACHMENT_UPLOAD_CONCURRENCY,
|
||||
uploadCodingAttachment,
|
||||
type CodingAttachmentRef,
|
||||
} from '@/lib/coding-attachments';
|
||||
@@ -23,7 +25,7 @@ import {
|
||||
useCodingConversationStore,
|
||||
type CodingConversationStoreState,
|
||||
} from '@/stores/coding-conversations';
|
||||
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import { codingWorkspaceStore, useCodingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import type {
|
||||
CodingDraftAttachment,
|
||||
} from '@/types/coding-conversation';
|
||||
@@ -66,6 +68,7 @@ function requestCount(
|
||||
}
|
||||
|
||||
const BLOCKING_REQUEST_STATUSES = new Set(['pending', 'accepted']);
|
||||
const SUBMITTING_REQUEST_STATUSES = new Set(['pending']);
|
||||
const ACCEPTED_REQUEST_STATUSES = new Set(['accepted']);
|
||||
const UNCERTAIN_REQUEST_STATUSES = new Set(['uncertain']);
|
||||
|
||||
@@ -96,10 +99,8 @@ export function CodingChatPanel({
|
||||
const submitPrompt = useCodingConversationStore((state) => state.submitPrompt);
|
||||
const recoverConversation = useCodingConversationStore((state) => state.recoverConversation);
|
||||
|
||||
const [provisionalDraft, setProvisionalDraft] = useState('');
|
||||
const [creatingNewConversation, setCreatingNewConversation] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submissionError, setSubmissionError] = useState<string | null>(null);
|
||||
const [provisionalDrafts, setProvisionalDrafts] = useState<Record<string, string>>({});
|
||||
const [submissionErrors, setSubmissionErrors] = useState<Record<string, string>>({});
|
||||
const [attachmentsByDraftKey, setAttachmentsByDraftKey] = useState<
|
||||
Record<string, LocalComposerAttachment[]>
|
||||
>({});
|
||||
@@ -108,6 +109,7 @@ export function CodingChatPanel({
|
||||
const attachmentsRef = useRef(attachmentsByDraftKey);
|
||||
const uploadedAttachmentsRef = useRef(new Map<string, CodingAttachmentRef>());
|
||||
const uploadFlightsRef = useRef(new Map<string, Promise<CodingAttachmentRef>>());
|
||||
const submissionFlightsRef = useRef(new Set<string>());
|
||||
|
||||
const agents = useMemo(() => (
|
||||
config?.agents.filter((agent) => agent.enabled && !agent.archivedAt) ?? []
|
||||
@@ -122,11 +124,16 @@ export function CodingChatPanel({
|
||||
), [conversations, selectedAgent]);
|
||||
const selectedConversation = conversations.find((conversation) => (
|
||||
conversation.id === selectedConversationId
|
||||
&& conversation.agentId === selectedAgent?.id
|
||||
&& !conversation.archivedAt
|
||||
)) ?? null;
|
||||
const targetConversationId = selectedConversation?.id ?? null;
|
||||
const provisionalDraftKey = activeProject && selectedAgent
|
||||
? `new:${activeProject.id}:${selectedAgent.id}`
|
||||
: null;
|
||||
const draftKey = selectedConversationId ?? provisionalDraftKey;
|
||||
const draftKey = targetConversationId ?? provisionalDraftKey;
|
||||
const provisionalDraft = provisionalDraftKey ? provisionalDrafts[provisionalDraftKey] ?? '' : '';
|
||||
const submissionError = draftKey ? submissionErrors[draftKey] ?? null : null;
|
||||
const localAttachments = useMemo(() => (
|
||||
draftKey ? attachmentsByDraftKey[draftKey] ?? [] : []
|
||||
), [attachmentsByDraftKey, draftKey]);
|
||||
@@ -134,42 +141,47 @@ export function CodingChatPanel({
|
||||
attachmentsRef.current = attachmentsByDraftKey;
|
||||
|
||||
const selectDraft = useCallback((state: CodingConversationStoreState) => (
|
||||
selectedConversationId
|
||||
? selectCodingConversationDraft(selectedConversationId)(state)
|
||||
targetConversationId
|
||||
? selectCodingConversationDraft(targetConversationId)(state)
|
||||
: null
|
||||
), [selectedConversationId]);
|
||||
), [targetConversationId]);
|
||||
const selectedDraft = useCodingConversationStore(selectDraft);
|
||||
const selectSnapshot = useCallback((state: CodingConversationStoreState) => (
|
||||
selectedConversationId
|
||||
? selectCodingConversationSnapshot(selectedConversationId)(state)
|
||||
targetConversationId
|
||||
? selectCodingConversationSnapshot(targetConversationId)(state)
|
||||
: null
|
||||
), [selectedConversationId]);
|
||||
), [targetConversationId]);
|
||||
const snapshot = useCodingConversationStore(selectSnapshot);
|
||||
const entryLoadState = useCodingConversationStore((state) => (
|
||||
selectedConversationId
|
||||
? state.entriesByConversationId[selectedConversationId]?.loadState ?? 'empty'
|
||||
targetConversationId
|
||||
? state.entriesByConversationId[targetConversationId]?.loadState ?? 'empty'
|
||||
: 'empty'
|
||||
));
|
||||
const entryError = useCodingConversationStore((state) => (
|
||||
selectedConversationId
|
||||
? state.entriesByConversationId[selectedConversationId]?.error ?? null
|
||||
targetConversationId
|
||||
? state.entriesByConversationId[targetConversationId]?.error ?? null
|
||||
: null
|
||||
));
|
||||
const blockingRequestCount = useCodingConversationStore((state) => requestCount(
|
||||
state,
|
||||
selectedConversationId,
|
||||
targetConversationId,
|
||||
BLOCKING_REQUEST_STATUSES,
|
||||
));
|
||||
const acceptedRequestCount = useCodingConversationStore((state) => requestCount(
|
||||
state,
|
||||
selectedConversationId,
|
||||
targetConversationId,
|
||||
ACCEPTED_REQUEST_STATUSES,
|
||||
));
|
||||
const uncertainRequestCount = useCodingConversationStore((state) => requestCount(
|
||||
state,
|
||||
selectedConversationId,
|
||||
targetConversationId,
|
||||
UNCERTAIN_REQUEST_STATUSES,
|
||||
));
|
||||
const submittingRequestCount = useCodingConversationStore((state) => requestCount(
|
||||
state,
|
||||
targetConversationId,
|
||||
SUBMITTING_REQUEST_STATUSES,
|
||||
));
|
||||
|
||||
useEffect(() => {
|
||||
void loadWorkspace().catch(() => undefined);
|
||||
@@ -207,7 +219,11 @@ export function CodingChatPanel({
|
||||
void ensureConversation(selectedAgent.id)
|
||||
.then((conversation) => {
|
||||
primeConversation(createLocalConversationSnapshot(activeProject.id, conversation));
|
||||
void selectConversation(conversation.id).catch(() => undefined);
|
||||
const current = codingWorkspaceStore.getState();
|
||||
if (current.activeProjectId === activeProject.id
|
||||
&& current.selectedAgentId === selectedAgent.id) {
|
||||
void selectConversation(conversation.id).catch(() => undefined);
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, [
|
||||
@@ -225,30 +241,38 @@ export function CodingChatPanel({
|
||||
const nextDraft = navigationDraft?.trim();
|
||||
if (!nextDraft || appliedNavigationDraftRef.current === nextDraft) return;
|
||||
appliedNavigationDraftRef.current = nextDraft;
|
||||
if (selectedConversationId) {
|
||||
setConversationDraft(selectedConversationId, nextDraft);
|
||||
if (targetConversationId) {
|
||||
setConversationDraft(targetConversationId, nextDraft);
|
||||
} else if (provisionalDraftKey) {
|
||||
setProvisionalDrafts((current) => ({ ...current, [provisionalDraftKey]: nextDraft }));
|
||||
} else {
|
||||
setProvisionalDraft(nextDraft);
|
||||
return;
|
||||
}
|
||||
}, [navigationDraft, selectedConversationId, setConversationDraft]);
|
||||
}, [navigationDraft, provisionalDraftKey, setConversationDraft, targetConversationId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedConversationId) return;
|
||||
if (!targetConversationId) return;
|
||||
if (provisionalDraft.trim()) {
|
||||
const current = codingConversationStore.getState().draftsByConversationId[selectedConversationId];
|
||||
if (!current?.text) setConversationDraft(selectedConversationId, provisionalDraft);
|
||||
setProvisionalDraft('');
|
||||
const current = codingConversationStore.getState().draftsByConversationId[targetConversationId];
|
||||
if (!current?.text) setConversationDraft(targetConversationId, provisionalDraft);
|
||||
if (provisionalDraftKey) {
|
||||
setProvisionalDrafts((drafts) => {
|
||||
const next = { ...drafts };
|
||||
delete next[provisionalDraftKey];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (provisionalDraftKey) {
|
||||
setAttachmentsByDraftKey((currentAttachments) => {
|
||||
const pending = currentAttachments[provisionalDraftKey];
|
||||
if (!pending?.length || currentAttachments[selectedConversationId]?.length) return currentAttachments;
|
||||
const next = { ...currentAttachments, [selectedConversationId]: pending };
|
||||
if (!pending?.length || currentAttachments[targetConversationId]?.length) return currentAttachments;
|
||||
const next = { ...currentAttachments, [targetConversationId]: pending };
|
||||
delete next[provisionalDraftKey];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}, [provisionalDraft, provisionalDraftKey, selectedConversationId, setConversationDraft]);
|
||||
}, [provisionalDraft, provisionalDraftKey, setConversationDraft, targetConversationId]);
|
||||
|
||||
const handleSelectConversation = useCallback((conversation: CodingConversationMetadata) => {
|
||||
if (!activeProject) return;
|
||||
@@ -257,19 +281,20 @@ export function CodingChatPanel({
|
||||
}, [activeProject, primeConversation, selectConversation]);
|
||||
|
||||
const handleCreateConversation = useCallback(async () => {
|
||||
if (!activeProject || !selectedAgent || creatingNewConversation) return;
|
||||
setCreatingNewConversation(true);
|
||||
try {
|
||||
const conversation = await createConversation(selectedAgent.id);
|
||||
primeConversation(createLocalConversationSnapshot(activeProject.id, conversation));
|
||||
if (!activeProject || !selectedAgent || creatingAgentIds[selectedAgent.id]) return;
|
||||
const projectId = activeProject.id;
|
||||
const agentId = selectedAgent.id;
|
||||
const conversation = await createConversation(agentId).catch(() => null);
|
||||
if (!conversation) return;
|
||||
primeConversation(createLocalConversationSnapshot(projectId, conversation));
|
||||
const current = codingWorkspaceStore.getState();
|
||||
if (current.activeProjectId === projectId && current.selectedAgentId === agentId) {
|
||||
await selectConversation(conversation.id).catch(() => undefined);
|
||||
} finally {
|
||||
setCreatingNewConversation(false);
|
||||
}
|
||||
}, [
|
||||
activeProject,
|
||||
createConversation,
|
||||
creatingNewConversation,
|
||||
creatingAgentIds,
|
||||
primeConversation,
|
||||
selectConversation,
|
||||
selectedAgent,
|
||||
@@ -278,14 +303,18 @@ export function CodingChatPanel({
|
||||
const handleAddFiles = useCallback((files: File[]) => {
|
||||
if (!draftKey) return;
|
||||
const accepted: LocalComposerAttachment[] = [];
|
||||
for (const file of files) {
|
||||
const remaining = Math.max(0, CODING_ATTACHMENT_MAX_COUNT - localAttachments.length);
|
||||
let validationError = files.length > remaining
|
||||
? `每条消息最多添加 ${CODING_ATTACHMENT_MAX_COUNT} 张图片。`
|
||||
: null;
|
||||
for (const file of files.slice(0, remaining)) {
|
||||
const mime = file.type.trim().toLowerCase();
|
||||
if (!CODING_ATTACHMENT_MIMES.has(mime)) {
|
||||
setSubmissionError('仅支持 PNG、JPEG、WebP 或 GIF 图片。');
|
||||
validationError = '仅支持 PNG、JPEG、WebP 或 GIF 图片。';
|
||||
continue;
|
||||
}
|
||||
if (file.size <= 0 || file.size > CODING_ATTACHMENT_MAX_BYTES) {
|
||||
setSubmissionError('图片不能超过 16 MB。');
|
||||
validationError = '图片不能超过 16 MB。';
|
||||
continue;
|
||||
}
|
||||
accepted.push({
|
||||
@@ -295,13 +324,18 @@ export function CodingChatPanel({
|
||||
previewUrl: URL.createObjectURL(file),
|
||||
});
|
||||
}
|
||||
setSubmissionErrors((current) => {
|
||||
const next = { ...current };
|
||||
if (validationError) next[draftKey] = validationError;
|
||||
else delete next[draftKey];
|
||||
return next;
|
||||
});
|
||||
if (accepted.length === 0) return;
|
||||
setSubmissionError(null);
|
||||
setAttachmentsByDraftKey((current) => ({
|
||||
...current,
|
||||
[draftKey]: [...(current[draftKey] ?? []), ...accepted],
|
||||
}));
|
||||
}, [draftKey]);
|
||||
}, [draftKey, localAttachments.length]);
|
||||
|
||||
const handleRemoveAttachment = useCallback((id: string) => {
|
||||
if (!draftKey) return;
|
||||
@@ -319,37 +353,54 @@ export function CodingChatPanel({
|
||||
|
||||
const prepareAttachments = useCallback(async (
|
||||
attachments: LocalComposerAttachment[],
|
||||
): Promise<CodingDraftAttachment[]> => await Promise.all(attachments.map(async (attachment) => {
|
||||
let uploaded = uploadedAttachmentsRef.current.get(attachment.id);
|
||||
if (!uploaded) {
|
||||
let flight = uploadFlightsRef.current.get(attachment.id);
|
||||
if (!flight) {
|
||||
flight = uploadCodingAttachment(attachment.file);
|
||||
uploadFlightsRef.current.set(attachment.id, flight);
|
||||
}
|
||||
try {
|
||||
uploaded = await flight;
|
||||
uploadedAttachmentsRef.current.set(attachment.id, uploaded);
|
||||
} finally {
|
||||
if (uploadFlightsRef.current.get(attachment.id) === flight) {
|
||||
uploadFlightsRef.current.delete(attachment.id);
|
||||
): Promise<CodingDraftAttachment[]> => {
|
||||
const prepared = new Array<CodingDraftAttachment>(attachments.length);
|
||||
let nextIndex = 0;
|
||||
const workers = Array.from({
|
||||
length: Math.min(CODING_ATTACHMENT_UPLOAD_CONCURRENCY, attachments.length),
|
||||
}, async () => {
|
||||
while (nextIndex < attachments.length) {
|
||||
const index = nextIndex++;
|
||||
const attachment = attachments[index];
|
||||
let uploaded = uploadedAttachmentsRef.current.get(attachment.id);
|
||||
if (!uploaded) {
|
||||
let flight = uploadFlightsRef.current.get(attachment.id);
|
||||
if (!flight) {
|
||||
flight = uploadCodingAttachment(attachment.file);
|
||||
uploadFlightsRef.current.set(attachment.id, flight);
|
||||
}
|
||||
try {
|
||||
uploaded = await flight;
|
||||
uploadedAttachmentsRef.current.set(attachment.id, uploaded);
|
||||
} finally {
|
||||
if (uploadFlightsRef.current.get(attachment.id) === flight) {
|
||||
uploadFlightsRef.current.delete(attachment.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
prepared[index] = {
|
||||
attachmentId: uploaded.attachmentId,
|
||||
mime: uploaded.mime,
|
||||
previewUrl: attachment.previewUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
attachmentId: uploaded.attachmentId,
|
||||
mime: uploaded.mime,
|
||||
previewUrl: attachment.previewUrl,
|
||||
};
|
||||
})), []);
|
||||
});
|
||||
await Promise.all(workers);
|
||||
return prepared;
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!selectedConversationId || !draftKey || submitting) return;
|
||||
if (!targetConversationId || !draftKey || submissionFlightsRef.current.has(targetConversationId)) return;
|
||||
const conversationId = targetConversationId;
|
||||
const attachments = [...localAttachments];
|
||||
setSubmitting(true);
|
||||
setSubmissionError(null);
|
||||
submissionFlightsRef.current.add(conversationId);
|
||||
setSubmissionErrors((current) => {
|
||||
const next = { ...current };
|
||||
delete next[draftKey];
|
||||
return next;
|
||||
});
|
||||
void submitPrompt({
|
||||
conversationId: selectedConversationId,
|
||||
conversationId,
|
||||
mode: 'prompt',
|
||||
prepareAttachments: attachments.length > 0
|
||||
? () => prepareAttachments(attachments)
|
||||
@@ -365,17 +416,19 @@ export function CodingChatPanel({
|
||||
uploadedAttachmentsRef.current.delete(attachment.id);
|
||||
}
|
||||
}).catch((error) => {
|
||||
setSubmissionError(error instanceof Error ? error.message : String(error));
|
||||
setSubmissionErrors((current) => ({
|
||||
...current,
|
||||
[draftKey]: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
}).finally(() => {
|
||||
setSubmitting(false);
|
||||
submissionFlightsRef.current.delete(conversationId);
|
||||
});
|
||||
}, [
|
||||
draftKey,
|
||||
localAttachments,
|
||||
prepareAttachments,
|
||||
selectedConversationId,
|
||||
targetConversationId,
|
||||
submitPrompt,
|
||||
submitting,
|
||||
]);
|
||||
|
||||
const runStatus = snapshot?.run.status ?? 'idle';
|
||||
@@ -392,10 +445,10 @@ export function CodingChatPanel({
|
||||
const editable = Boolean(activeProject && selectedAgent);
|
||||
const canSend = Boolean(
|
||||
editable
|
||||
&& selectedConversationId
|
||||
&& targetConversationId
|
||||
&& (draft.trim() || localAttachments.length > 0)
|
||||
&& !running
|
||||
&& !submitting
|
||||
&& submittingRequestCount === 0
|
||||
&& !preparationError
|
||||
&& blockingRequestCount === 0
|
||||
&& uncertainRequestCount === 0,
|
||||
@@ -439,7 +492,10 @@ export function CodingChatPanel({
|
||||
'flex min-h-10 w-full items-center gap-2 rounded-xl px-2.5 text-left text-sm transition-[background-color,scale] duration-150 ease-out active:scale-[0.96]',
|
||||
selectedAgent?.id === agent.id ? 'bg-background font-medium shadow-soft' : 'hover:bg-background/70',
|
||||
)}
|
||||
onClick={() => selectAgent(agent.id)}
|
||||
onClick={() => {
|
||||
clearConversationSelection();
|
||||
selectAgent(agent.id);
|
||||
}}
|
||||
>
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-foreground text-xs font-semibold text-background">
|
||||
{agent.name.trim().slice(0, 1) || 'A'}
|
||||
@@ -456,10 +512,10 @@ export function CodingChatPanel({
|
||||
size="icon"
|
||||
className="h-10 w-10 rounded-xl transition-transform duration-150 ease-out active:scale-[0.96]"
|
||||
aria-label="新建对话"
|
||||
disabled={!selectedAgent || creatingNewConversation}
|
||||
disabled={!selectedAgent || Boolean(selectedAgent && creatingAgentIds[selectedAgent.id])}
|
||||
onClick={() => void handleCreateConversation()}
|
||||
>
|
||||
{creatingNewConversation
|
||||
{selectedAgent && creatingAgentIds[selectedAgent.id]
|
||||
? <LoaderCircle className="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
: <MessageSquarePlus className="h-4 w-4" aria-hidden="true" />}
|
||||
</Button>
|
||||
@@ -471,7 +527,7 @@ export function CodingChatPanel({
|
||||
type="button"
|
||||
className={cn(
|
||||
'min-h-10 w-full truncate rounded-xl px-3 text-left text-sm transition-[background-color,scale] duration-150 ease-out active:scale-[0.96]',
|
||||
selectedConversationId === conversation.id
|
||||
targetConversationId === conversation.id
|
||||
? 'bg-background font-medium shadow-soft'
|
||||
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground',
|
||||
)}
|
||||
@@ -530,11 +586,11 @@ export function CodingChatPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedConversationId
|
||||
{targetConversationId
|
||||
? (
|
||||
<CodingConversationTimeline
|
||||
key={selectedConversationId}
|
||||
conversationId={selectedConversationId}
|
||||
key={targetConversationId}
|
||||
conversationId={targetConversationId}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
@@ -552,17 +608,25 @@ export function CodingChatPanel({
|
||||
recoverableError={!submissionError && Boolean(preparationError)}
|
||||
acceptedCount={acceptedRequestCount}
|
||||
attachments={localAttachments.map(({ id, name, previewUrl }) => ({ id, name, previewUrl }))}
|
||||
submitting={submitting}
|
||||
submitting={submittingRequestCount > 0}
|
||||
placeholder={selectedAgent ? '给当前对话发送消息' : '请选择一个伙伴后再发送'}
|
||||
onChange={(value) => {
|
||||
setSubmissionError(null);
|
||||
if (selectedConversationId) setConversationDraft(selectedConversationId, value);
|
||||
else setProvisionalDraft(value);
|
||||
if (draftKey) {
|
||||
setSubmissionErrors((current) => {
|
||||
const next = { ...current };
|
||||
delete next[draftKey];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
if (targetConversationId) setConversationDraft(targetConversationId, value);
|
||||
else if (provisionalDraftKey) {
|
||||
setProvisionalDrafts((current) => ({ ...current, [provisionalDraftKey]: value }));
|
||||
}
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
onRecover={() => {
|
||||
if (selectedConversationId) {
|
||||
void recoverConversation(selectedConversationId).catch(() => undefined);
|
||||
if (targetConversationId) {
|
||||
void recoverConversation(targetConversationId).catch(() => undefined);
|
||||
}
|
||||
}}
|
||||
onAddFiles={handleAddFiles}
|
||||
|
||||
@@ -338,6 +338,7 @@ export function createCodingConversationStore(
|
||||
const deps = { ...defaultDependencies(), ...dependencies };
|
||||
const snapshotLoads = new Map<string, Promise<ConversationSnapshot>>();
|
||||
const recoveryBatches = new Map<string, CodingConversationPatchBatchEvent[]>();
|
||||
const recoveryRefreshKeys = new Map<string, string>();
|
||||
let eventSource: EventSource | null = null;
|
||||
let connectFlight: Promise<void> | null = null;
|
||||
let connectionGeneration = 0;
|
||||
@@ -375,11 +376,20 @@ export function createCodingConversationStore(
|
||||
const items = [...itemsBySeq.values()].sort((left, right) => left.seq - right.seq);
|
||||
if (items.length === 0) {
|
||||
recoveryBatches.delete(conversationId);
|
||||
recoveryRefreshKeys.delete(conversationId);
|
||||
return;
|
||||
}
|
||||
const continuous = items[0].seq === snapshot.cursor.seq + 1
|
||||
&& items.every((item, index) => index === 0 || item.seq === items[index - 1].seq + 1);
|
||||
if (!continuous) {
|
||||
const lastSeq = items.at(-1)?.seq ?? items[0].seq;
|
||||
const refreshKey = [
|
||||
snapshot.cursor.workerGeneration,
|
||||
snapshot.cursor.seq,
|
||||
items[0].seq,
|
||||
lastSeq,
|
||||
].join(':');
|
||||
const willRefresh = recoveryRefreshKeys.get(conversationId) !== refreshKey;
|
||||
store.setState((state) => {
|
||||
const current = state.entriesByConversationId[conversationId] ?? emptyEntry();
|
||||
return {
|
||||
@@ -387,15 +397,24 @@ export function createCodingConversationStore(
|
||||
...state.entriesByConversationId,
|
||||
[conversationId]: {
|
||||
...current,
|
||||
loadState: 'recovering',
|
||||
loadState: willRefresh ? 'recovering' : 'error',
|
||||
error: 'Conversation patch batch is not continuous',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
if (willRefresh) {
|
||||
recoveryRefreshKeys.set(conversationId, refreshKey);
|
||||
queueMicrotask(() => {
|
||||
if (!snapshotLoads.has(conversationId)) {
|
||||
void store.getState().loadSnapshot(conversationId, true).catch(() => undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
recoveryBatches.delete(conversationId);
|
||||
recoveryRefreshKeys.delete(conversationId);
|
||||
store.getState().applyPatchBatchEvent({
|
||||
type: 'patch-batch',
|
||||
conversationId,
|
||||
@@ -521,6 +540,7 @@ export function createCodingConversationStore(
|
||||
},
|
||||
|
||||
async recoverConversation(conversationId) {
|
||||
recoveryRefreshKeys.delete(conversationId);
|
||||
set((state) => {
|
||||
const entry = state.entriesByConversationId[conversationId] ?? emptyEntry();
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user