diff --git a/.project-docs/30-worklog/tasks/20260823-pi-chat-timeline-c8f31a62.md b/.project-docs/30-worklog/tasks/20260823-pi-chat-timeline-c8f31a62.md index e09f148..253fb89 100644 --- a/.project-docs/30-worklog/tasks/20260823-pi-chat-timeline-c8f31a62.md +++ b/.project-docs/30-worklog/tasks/20260823-pi-chat-timeline-c8f31a62.md @@ -151,6 +151,14 @@ behind a buffered continuous tail, Renderer requests one fresh target Snapshot; a repeated identical stale response becomes a retryable error instead of remaining in an infinite recovering state. +- Closed the incremental planner review findings after `bec93d0`: attachment + editing is locked for the originating Conversation until upload and HTTP 202 + acceptance settle, so removal or a next-message paste cannot mutate the + captured image set. Attachment preparation failures now remain local to the + Composer, restore the draft, permit direct retry without runtime recovery, + and retain already uploaded variants for reuse. Attachment route failures use + only registered `CODING_ATTACHMENT_INVALID`, `CODING_ATTACHMENT_NOT_FOUND`, + and `CODING_STORAGE_WRITE_FAILED` codes while preserving 413/404 status. - Synchronized `README.md` with the current Pi core Chat, first-Conversation, batch and attachment behavior. PI-130 feature controls and PI-140 legacy removal remain outside this task. @@ -161,6 +169,10 @@ **Standards Needs Fix / Spec Needs Fix**. PI-120 remained the unique Ready Frontier; the findings above were treated as blocking rather than advancing PI-130. +- The correction candidate `bec93d0` received incremental planner review result + **Standards PASS / Spec Needs Fix**. Its three remaining attachment-state and + typed-error findings were implemented and kept PI-130 locked pending another + planner review. - Post-review focused suites passed: attachment Host routes, stale recovery, shared ProjectService/Registry mutation queue, slow Agent switch, cross- Conversation submission isolation, attachment count/concurrency and pressure @@ -171,7 +183,11 @@ 20 React commits, 131,148 wire bytes, and measured Main-to-React p95 `35.995 ms` against the `<=50 ms` budget. It includes mixed message blocks, 4 KiB tool output and more than 100 KiB of cumulative thinking output. -- `pnpm test`: passed, 218 files / 2316 passed / 2 skipped. +- Latest attachment-state focused run passed: 3 files / 39 tests, including + delayed upload/removal, delayed HTTP 202/next-image isolation, local + preparation failure/direct retry, uploaded-variant reuse, and registered + Host error codes. +- `pnpm test`: passed, 218 files / 2321 passed / 2 skipped. - `pnpm run typecheck`: passed. - `pnpm run lint:check`: passed with 0 errors and six unchanged warnings in `ExecutionGraphCard`, `Home`, and `Makelore`. diff --git a/electron/api/routes/coding-attachments.ts b/electron/api/routes/coding-attachments.ts index 4dee2a3..f9de310 100644 --- a/electron/api/routes/coding-attachments.ts +++ b/electron/api/routes/coding-attachments.ts @@ -66,28 +66,32 @@ async function readBoundedBody(req: IncomingMessage): Promise { return Buffer.concat(chunks); } -function sendAttachmentError(res: ServerResponse, error: unknown): void { - const status = typeof (error as { status?: unknown })?.status === 'number' +function sendAttachmentError( + res: ServerResponse, + error: unknown, + operation: 'upload' | 'content', +): void { + const explicitStatus = typeof (error as { status?: unknown })?.status === 'number' ? (error as { status: number }).status - : (error as NodeJS.ErrnoException)?.code === 'ENOENT' - ? 404 - : 500; + : null; + const status = explicitStatus + ?? (operation === 'content' || (error as NodeJS.ErrnoException)?.code === 'ENOENT' ? 404 : 500); const message = status === 413 ? '图片不能超过 16 MB。' : status === 404 ? '图片附件不存在。' : status === 400 ? '图片附件无效。' - : '图片附件暂时无法读取。'; + : operation === 'upload' + ? '本地数据写入失败,请检查存储后重试。' + : '图片附件暂时无法读取。'; sendJson(res, status, { success: false, - code: status === 413 - ? 'CODING_ATTACHMENT_TOO_LARGE' + code: status === 400 || status === 413 + ? 'CODING_ATTACHMENT_INVALID' : status === 404 ? 'CODING_ATTACHMENT_NOT_FOUND' - : status === 400 - ? 'CODING_ATTACHMENT_INVALID' - : 'CODING_ATTACHMENT_STORAGE_FAILED', + : 'CODING_STORAGE_WRITE_FAILED', error: message, }); } @@ -147,7 +151,7 @@ export async function handleCodingAttachmentRoutes( res.end(record.data); return true; } catch (error) { - sendAttachmentError(res, error); + sendAttachmentError(res, error, upload ? 'upload' : 'content'); return true; } } diff --git a/src/pages/Chat/CodingChatPanel.tsx b/src/pages/Chat/CodingChatPanel.tsx index 749688c..318ac28 100644 --- a/src/pages/Chat/CodingChatPanel.tsx +++ b/src/pages/Chat/CodingChatPanel.tsx @@ -301,7 +301,8 @@ export function CodingChatPanel({ ]); const handleAddFiles = useCallback((files: File[]) => { - if (!draftKey) return; + if (!draftKey + || (targetConversationId && submissionFlightsRef.current.has(targetConversationId))) return; const accepted: LocalComposerAttachment[] = []; const remaining = Math.max(0, CODING_ATTACHMENT_MAX_COUNT - localAttachments.length); let validationError = files.length > remaining @@ -335,10 +336,11 @@ export function CodingChatPanel({ ...current, [draftKey]: [...(current[draftKey] ?? []), ...accepted], })); - }, [draftKey, localAttachments.length]); + }, [draftKey, localAttachments.length, targetConversationId]); const handleRemoveAttachment = useCallback((id: string) => { - if (!draftKey) return; + if (!draftKey + || (targetConversationId && submissionFlightsRef.current.has(targetConversationId))) return; setAttachmentsByDraftKey((current) => { const attachment = current[draftKey]?.find((item) => item.id === id); if (attachment) URL.revokeObjectURL(attachment.previewUrl); @@ -349,7 +351,12 @@ export function CodingChatPanel({ }); uploadedAttachmentsRef.current.delete(id); uploadFlightsRef.current.delete(id); - }, [draftKey]); + setSubmissionErrors((current) => { + const next = { ...current }; + delete next[draftKey]; + return next; + }); + }, [draftKey, targetConversationId]); const prepareAttachments = useCallback(async ( attachments: LocalComposerAttachment[], diff --git a/src/pages/Chat/CodingComposer.tsx b/src/pages/Chat/CodingComposer.tsx index 55d9ae5..830f4ee 100644 --- a/src/pages/Chat/CodingComposer.tsx +++ b/src/pages/Chat/CodingComposer.tsx @@ -91,6 +91,7 @@ export function CodingComposer({ type="file" accept="image/png,image/jpeg,image/webp,image/gif" multiple + disabled={!editable || submitting} className="sr-only" data-testid="coding-file-attachment-input" onChange={(event) => { @@ -110,8 +111,9 @@ export function CodingComposer({ />