From 403236115bf81e7617856cd8219b1be23f6abbb8 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Fri, 4 Sep 2026 08:38:13 +0800 Subject: [PATCH] fix(design): show submitted messages immediately --- ...0904-design-summary-separation-9c4e7a21.md | 38 ++++++++- .../ImageCanvas/DesignConversationPane.tsx | 61 ++++++++++++--- tests/unit/image-canvas-page.test.tsx | 78 ++++++++++++++++++- tests/unit/image-workspace-store.test.ts | 44 +++++++++++ 4 files changed, 208 insertions(+), 13 deletions(-) diff --git a/.project-docs/30-worklog/tasks/20260904-design-summary-separation-9c4e7a21.md b/.project-docs/30-worklog/tasks/20260904-design-summary-separation-9c4e7a21.md index fb61e52..79f963c 100644 --- a/.project-docs/30-worklog/tasks/20260904-design-summary-separation-9c4e7a21.md +++ b/.project-docs/30-worklog/tasks/20260904-design-summary-separation-9c4e7a21.md @@ -18,6 +18,10 @@ canonical turn arrives. - Close the supported duplicate-stream path caused by overlapping event-source opens and repeated/out-of-order `chunkIndex` delivery. +- Project a newly submitted chat command into the conversation immediately, before the + server returns the canonical turn. Keep the provisional message visually honest, + replace it with the canonical turn without duplication, and restore the draft after + a definitive failure. - Add focused Renderer and Store regression tests. Do not change Works Square server, Electron Main DTO/API, Current Specification authority, Quote/generation, Plugin navigation, packaging, publication, or the three foreign root task records. @@ -37,6 +41,8 @@ right-side “AI 听懂的想法” remains its secondary projection. - Keep Chinese-only copy, reduced-motion behavior, at least 40 px interactive targets, and the current responsive single-mount behavior. +- Do not create a second chat state machine: the existing pending operation is the + provisional message identity, while only the returned Workspace turn is canonical. ## Project Context Loaded @@ -60,6 +66,17 @@ ## Outcome +- A chat command now appears in the conversation immediately from its existing + `PendingDesignOperation`, before the server returns the canonical Workspace turn. + The provisional user bubble carries a small `发送中` state, changes to `正在确认` + for an unknown accepted outcome, and is filtered to the active Workspace. +- While that exact draft is projected as a provisional bubble, the composer looks + cleared without deleting the stored draft. Canonical success atomically replaces + the provisional bubble with the persisted turn; definitive failure removes the + pending projection and naturally restores the original draft for correction/retry. +- First-use guidance is hidden as soon as the first provisional user message exists. + Other in-flight Design input commands retain the existing composer lock, so the UX + change does not introduce concurrent reducer writes. - `DesignConversationPane` no longer renders raw `assistantStreams` prose as an assistant chat bubble. It renders one compact `role=status` surface that explicitly says the AI is organizing the idea and that the content is not a new reply. A @@ -79,6 +96,20 @@ ## Verification +- New optimistic-message red baseline: Store publication passed, while the Renderer + regression failed because no `发送中` message existed, the first-use prompt remained, + and the composer still showed the submitted draft. After the fix, the two focused + files pass `27/27`, including submitting → unknown → canonical replacement. +- Adjacent ImageCanvas, Store, Fine Tune, youth summary/projection/copy tests now pass + `6 files / 51 tests`. +- Follow-up TypeScript `tsc --noEmit`, changed-file ESLint, and production + Renderer/Main/Preload/utility Vite build pass. Build output contains only the + existing browserslist, dynamic-import, and large-chunk warnings. +- Electron/Playwright AI Design checks both pass individually: Prompt Museum media + isolation passed in the two-test run; the conversation/adjustment/Quote/Task flow + initially timed out at the pre-Design auth-fixture readiness gate, then passed `1/1` + in isolation in 1.8 seconds. The timeout occurred before this feature path and is + not recorded as product success evidence for the new provisional state. - Red baseline: three new regressions failed against the original implementation—raw unfinished prose had no progress role, an obsolete concurrent EventSource was not closed, and replaying chunk index `0` produced duplicated text. A fourth targeted red @@ -108,8 +139,11 @@ - Proposal: record that `design.assistant.delta` is unfinished progress, never a committed conversation turn. Renderer may show an explicit progress/uncertainty status but must not render its raw prose as a chat reply; only canonical persisted - turns enter the timeline. Concurrent/replayed stream delivery must converge by - connection generation and `chunkIndex`. + turns enter the timeline. A locally submitted chat command may appear immediately as + a clearly provisional user bubble by projecting its existing pending operation; it + must disappear when the canonical turn arrives and must not become a second semantic + message store. Concurrent/replayed stream delivery must converge by connection + generation and `chunkIndex`. - Evidence: the user screenshot, four red-before/green-after regressions, 49 adjacent tests, typecheck, scoped lint, production build, and 2/2 Electron E2E. - Future impact: later streaming polish must preserve the distinction between transient diff --git a/src/pages/ImageCanvas/DesignConversationPane.tsx b/src/pages/ImageCanvas/DesignConversationPane.tsx index 2422565..1b6488a 100644 --- a/src/pages/ImageCanvas/DesignConversationPane.tsx +++ b/src/pages/ImageCanvas/DesignConversationPane.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { Loader2, MessageSquareText, Send, Sparkles } from 'lucide-react'; +import { Clock3, Loader2, MessageSquareText, Send, Sparkles } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; @@ -47,6 +47,19 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa const assistantStreams = useImageWorkspaceStore((state) => state.assistantStreams); const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations); const scrollAnchorRef = useRef(null); + const pendingChatMessages = Object.values(pendingOperations).flatMap((operation) => { + const command = operation.command; + if (command.kind !== 'apply_input' + || command.workspaceId !== workspace.workspace.workspaceId + || command.input.kind !== 'chat') { + return []; + } + return [{ + operationId: operation.id, + message: command.input.message, + status: operation.status, + }]; + }); const streamingOperationIds = Object.entries(assistantStreams) .filter(([, text]) => text.trim()) .map(([operationId]) => operationId); @@ -54,18 +67,24 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa const hasUnknownAssistantProgress = streamingOperationIds.some( (operationId) => pendingOperations[operationId]?.status === 'unknown', ); - const submittingChat = Object.values(pendingOperations).some( + const submittingDesignInput = Object.values(pendingOperations).some( (operation) => operation.status === 'submitting' && operation.command.kind === 'apply_input', ); + const projectedDraft = chatDraft.trim() !== '' + && pendingChatMessages.some((message) => message.message === chatDraft.trim()); + const composerDraft = projectedDraft ? '' : chatDraft; + const pendingChatKey = pendingChatMessages + .map((message) => `${message.operationId}:${message.status}`) + .join('|'); useEffect(() => { const scrollAnchor = scrollAnchorRef.current; if (typeof scrollAnchor?.scrollIntoView === 'function') { scrollAnchor.scrollIntoView({ block: 'end' }); } - }, [hasAssistantProgress, workspace.turns.length]); + }, [hasAssistantProgress, pendingChatKey, workspace.turns.length]); const submit = () => { - if (!chatDraft.trim() || submittingChat) return; + if (!composerDraft.trim() || submittingDesignInput) return; void sendChat().catch((error) => { toast.error(chatFailureMessage(error)); }); @@ -87,7 +106,9 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
- {workspace.turns.length === 0 && !hasAssistantProgress && ( + {workspace.turns.length === 0 + && pendingChatMessages.length === 0 + && !hasAssistantProgress && (
@@ -128,6 +149,26 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
))} + {pendingChatMessages.map((message) => ( +
+
+ {message.message} +
+
+ {message.status === 'submitting' + ?
+
+ ))} + {hasAssistantProgress && (