fix(pi): retain ownership for uncertain mutations
This commit is contained in:
@@ -53,6 +53,25 @@ interface LocalComposerAttachment {
|
||||
previewUrl: string;
|
||||
}
|
||||
|
||||
interface LocalSubmissionError {
|
||||
message: string;
|
||||
backendCode?: string;
|
||||
}
|
||||
|
||||
function localSubmissionError(error: unknown): LocalSubmissionError {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const details = error && typeof error === 'object'
|
||||
&& 'details' in error
|
||||
&& error.details
|
||||
&& typeof error.details === 'object'
|
||||
? error.details as { backendCode?: unknown }
|
||||
: null;
|
||||
return {
|
||||
message,
|
||||
...(typeof details?.backendCode === 'string' ? { backendCode: details.backendCode } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function newestConversation(
|
||||
conversations: CodingConversationMetadata[],
|
||||
agentId: string,
|
||||
@@ -124,7 +143,7 @@ export function CodingChatPanel({
|
||||
const conversationSummaries = useCodingConversationStore((state) => state.summariesByConversationId);
|
||||
|
||||
const [provisionalDrafts, setProvisionalDrafts] = useState<Record<string, string>>({});
|
||||
const [submissionErrors, setSubmissionErrors] = useState<Record<string, string>>({});
|
||||
const [submissionErrors, setSubmissionErrors] = useState<Record<string, LocalSubmissionError>>({});
|
||||
const [modesByDraftKey, setModesByDraftKey] = useState<Record<string, PromptMode>>({});
|
||||
const [inspectorOpen, setInspectorOpen] = useState(false);
|
||||
const [attachmentsByDraftKey, setAttachmentsByDraftKey] = useState<
|
||||
@@ -387,7 +406,7 @@ export function CodingChatPanel({
|
||||
}
|
||||
setSubmissionErrors((current) => {
|
||||
const next = { ...current };
|
||||
if (validationError) next[draftKey] = validationError;
|
||||
if (validationError) next[draftKey] = { message: validationError };
|
||||
else delete next[draftKey];
|
||||
return next;
|
||||
});
|
||||
@@ -488,7 +507,7 @@ export function CodingChatPanel({
|
||||
}).catch((error) => {
|
||||
setSubmissionErrors((current) => ({
|
||||
...current,
|
||||
[draftKey]: error instanceof Error ? error.message : String(error),
|
||||
[draftKey]: localSubmissionError(error),
|
||||
}));
|
||||
}).finally(() => {
|
||||
submissionFlightsRef.current.delete(conversationId);
|
||||
@@ -506,6 +525,17 @@ export function CodingChatPanel({
|
||||
const workerStatus = snapshot?.worker.status ?? 'stopped';
|
||||
const runtimeError = snapshot?.run.error ?? snapshot?.worker.error ?? null;
|
||||
const preparationError = entryError ?? runtimeError?.message ?? null;
|
||||
useEffect(() => {
|
||||
if (!draftKey
|
||||
|| uncertainRequestCount > 0
|
||||
|| runtimeError?.code === 'CODING_REQUEST_UNCERTAIN') return;
|
||||
setSubmissionErrors((current) => {
|
||||
if (current[draftKey]?.backendCode !== 'CODING_REQUEST_UNCERTAIN') return current;
|
||||
const next = { ...current };
|
||||
delete next[draftKey];
|
||||
return next;
|
||||
});
|
||||
}, [draftKey, runtimeError?.code, uncertainRequestCount]);
|
||||
const preparing = entryLoadState === 'loading'
|
||||
|| workerStatus === 'starting'
|
||||
|| workerStatus === 'recovering'
|
||||
@@ -744,7 +774,7 @@ export function CodingChatPanel({
|
||||
if (!draftKey) return;
|
||||
setSubmissionErrors((current) => ({
|
||||
...current,
|
||||
[draftKey]: error instanceof Error ? error.message : String(error),
|
||||
[draftKey]: localSubmissionError(error),
|
||||
}));
|
||||
});
|
||||
}}
|
||||
@@ -772,7 +802,7 @@ export function CodingChatPanel({
|
||||
runStatus={runStatus}
|
||||
mode={promptMode}
|
||||
queue={snapshot?.queue ?? { items: [] }}
|
||||
error={submissionError ?? preparationError}
|
||||
error={submissionError?.message ?? preparationError}
|
||||
recoverableError={!submissionError && Boolean(preparationError)}
|
||||
acceptedCount={acceptedRequestCount}
|
||||
attachments={localAttachments.map(({ id, name, previewUrl }) => ({ id, name, previewUrl }))}
|
||||
|
||||
@@ -170,7 +170,7 @@ export function CodingConversationHeader({
|
||||
value={modelValue}
|
||||
aria-label="当前对话模型"
|
||||
className="h-9 min-w-52 max-w-72 text-xs"
|
||||
disabled={Boolean(busyAction)}
|
||||
disabled={Boolean(busyAction) || running}
|
||||
onChange={(event) => {
|
||||
const selected = parseCodingModelKey(event.target.value);
|
||||
if (!selected) return;
|
||||
@@ -191,7 +191,7 @@ export function CodingConversationHeader({
|
||||
value={thinkingLevel}
|
||||
aria-label="当前对话思考级别"
|
||||
className="h-9 w-36 shrink-0 text-xs"
|
||||
disabled={Boolean(busyAction) || !model}
|
||||
disabled={Boolean(busyAction) || !model || running}
|
||||
onChange={(event) => perform('thinking', async () => {
|
||||
await setCodingConversationThinking(
|
||||
conversation.id,
|
||||
@@ -215,7 +215,7 @@ export function CodingConversationHeader({
|
||||
<RotateCcw className="mr-1.5 h-3.5 w-3.5" aria-hidden="true" />恢复
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" variant="ghost" className="min-h-9 shrink-0 rounded-xl px-3 text-xs" disabled={Boolean(busyAction)} onClick={() => perform('fork', onFork)}>
|
||||
<Button type="button" variant="ghost" className="min-h-9 shrink-0 rounded-xl px-3 text-xs" disabled={Boolean(busyAction) || running} onClick={() => perform('fork', onFork)}>
|
||||
<GitFork className="mr-1.5 h-3.5 w-3.5" aria-hidden="true" />创建分支
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" className="min-h-9 shrink-0 rounded-xl px-3 text-xs" disabled={Boolean(busyAction)} onClick={() => perform('unread', onToggleUnread)}>
|
||||
|
||||
@@ -163,9 +163,13 @@ function withoutReconciledRequests(
|
||||
snapshot: ConversationSnapshot,
|
||||
): Record<string, CodingPromptRequestState> {
|
||||
const reconciled = requestIdsInSnapshot(snapshot);
|
||||
if (!requests || reconciled.size === 0) return requests ?? {};
|
||||
const requestUncertaintySettled = snapshot.run.status === 'idle' || snapshot.run.status === 'error';
|
||||
if (!requests || (reconciled.size === 0 && !requestUncertaintySettled)) return requests ?? {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(requests).filter(([clientRequestId]) => !reconciled.has(clientRequestId)),
|
||||
Object.entries(requests).filter(([clientRequestId, request]) => (
|
||||
!reconciled.has(clientRequestId)
|
||||
&& !(request.status === 'uncertain' && requestUncertaintySettled)
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -962,21 +966,17 @@ export function createCodingConversationStore(
|
||||
unread,
|
||||
};
|
||||
const requests = state.requestsByConversationId[event.conversationId] ?? {};
|
||||
const reconciledRequestIds = new Set(incomingMessages.flatMap((message) => (
|
||||
message.clientRequestId ? [message.clientRequestId] : []
|
||||
)));
|
||||
const nextRequests = reconciledRequestIds.size > 0
|
||||
? Object.fromEntries(
|
||||
Object.entries(requests).filter(([id]) => !reconciledRequestIds.has(id)),
|
||||
)
|
||||
const nextRequests = reducer.snapshot
|
||||
? withoutReconciledRequests(requests, reducer.snapshot)
|
||||
: requests;
|
||||
const requestsChanged = Object.keys(nextRequests).length !== Object.keys(requests).length;
|
||||
return {
|
||||
entriesByConversationId: {
|
||||
...state.entriesByConversationId,
|
||||
[event.conversationId]: entry,
|
||||
},
|
||||
summariesByConversationId: summariesWithEntry(state.summariesByConversationId, entry),
|
||||
requestsByConversationId: nextRequests === requests
|
||||
requestsByConversationId: !requestsChanged
|
||||
? state.requestsByConversationId
|
||||
: {
|
||||
...state.requestsByConversationId,
|
||||
|
||||
Reference in New Issue
Block a user