merge: integrate Design chat generation handoff client
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
# Task: Handle AI Design chat generation handoff
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- Task ID: 20260907-design-chat-generation-handoff-client-7d3a9c41
|
||||||
|
- Mode: Feature
|
||||||
|
- Branch: codex/20260907-design-chat-generation-handoff-client-7d3a9c41-design-chat-generation-handoff
|
||||||
|
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260907-design-chat-generation-handoff-client-7d3a9c41
|
||||||
|
- Base commit: bce20d9e4dad99565714bd2c0ca1ef4806426f57
|
||||||
|
- Owner: codex-root
|
||||||
|
- Status: Ready for Integration
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Verify that the existing client event contract can consume a server-owned
|
||||||
|
`开始生成` handoff without introducing a second Renderer command or public DTO.
|
||||||
|
- Preserve the original optimistic user message, activity/reply settlement, stable
|
||||||
|
operation identity, and the offered Quote created immediately after the committed
|
||||||
|
chat event.
|
||||||
|
- Add focused Store coverage for two consecutive Direction projections belonging to
|
||||||
|
the same accepted chat command.
|
||||||
|
|
||||||
|
## Intent And Constraints
|
||||||
|
|
||||||
|
- Electron Main remains the only network authority. Source inspection showed that no
|
||||||
|
new Renderer orchestration is needed: the server can emit the committed chat and
|
||||||
|
subsequent Quote as two existing `design.direction.updated` events in one Run.
|
||||||
|
- Never infer generation intent from assistant prose or duplicate phrase matching in
|
||||||
|
the client. The server owns interpretation and the Quote transaction.
|
||||||
|
- Keep Quote and generation confirmation separate: automatic handoff may create an
|
||||||
|
immutable Quote, but only explicit user confirmation may create a Task or reserve
|
||||||
|
Token Points.
|
||||||
|
- Derive a stable follow-up operation identity from the committed chat Interaction,
|
||||||
|
and do not replay an already accepted/unknown mutation under a new identity.
|
||||||
|
|
||||||
|
## Project Context Loaded
|
||||||
|
|
||||||
|
- Task context: feature task `20260907-design-chat-generation-handoff-client-7d3a9c41`
|
||||||
|
owns this managed linked worktree at base `bce20d9e…`.
|
||||||
|
- Read: agent-entry and planning-gate docs, memory index, project positioning,
|
||||||
|
integrated current state, decision index/system overview, ADR-007, architecture
|
||||||
|
module/data-flow maps, domain/success criteria, evidence/reflection/commitment/stale
|
||||||
|
indexes, and the recent Design activity and repeated-turn peer task records.
|
||||||
|
- Project goal: a youth-friendly Canvas whose Current Specification, immutable Quote,
|
||||||
|
and confirmed Task remain server-authoritative.
|
||||||
|
- Current integrated focus: optimistic chat, operation-scoped activity and committed
|
||||||
|
assistant streaming are already present; repeated identical user text is valid.
|
||||||
|
- Decisions affecting this task: ADR-007 requires stable operation identity, Main-owned
|
||||||
|
transport, immutable Quote display, and explicit Quote-ID confirmation.
|
||||||
|
- Evidence and commitments: production logs show an input Run completed without any
|
||||||
|
Quote/Task command; the real paid confirmation smoke remains separately pending.
|
||||||
|
- Likely modules: `shared/image-workspace.ts`, the Works Square adapter, Renderer
|
||||||
|
image-workspace Store, and their focused tests.
|
||||||
|
- Unknowns/conflicts: source inspection must identify the narrowest post-commit event
|
||||||
|
seam and ensure reconnect/replay cannot create duplicate Quote requests. The root
|
||||||
|
worktree's unrelated untracked records are outside this isolated task. No active
|
||||||
|
peer has a conflicting Design scope.
|
||||||
|
- Gate result: Passed.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- Confirmed that Main maps every Gateway `design.direction.updated` event
|
||||||
|
independently and the Store applies canonical form projections even after the
|
||||||
|
original optimistic operation has settled.
|
||||||
|
- Added a Store regression for the real sequence: committed `chat` projection,
|
||||||
|
immediately followed by `quote_request` with the same client operation identity.
|
||||||
|
The test proves the draft/pending chat settles once, the completed activity remains
|
||||||
|
bound to its Turn, and the offered Quote remains visible.
|
||||||
|
- No client product code, shared contract, routing, or UI change was necessary.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `pnpm exec vitest run tests/unit/image-workspace-store.test.ts --maxWorkers=1`
|
||||||
|
passed (33 tests).
|
||||||
|
- `pnpm exec eslint tests/unit/image-workspace-store.test.ts` passed.
|
||||||
|
- `pnpm run typecheck` passed.
|
||||||
|
- `git diff --check` passed.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- Integrate and deploy with server task
|
||||||
|
`20260907-design-chat-generation-intent-4f8c2a91`, then perform the recorded real
|
||||||
|
Quote/confirmation smoke.
|
||||||
|
|
||||||
|
## Promotion Candidates
|
||||||
|
|
||||||
|
- Target: `.project-docs/20-architecture/data-flow.md` and integrated current state.
|
||||||
|
Proposal: record that one accepted Design chat Run may carry a committed chat
|
||||||
|
Direction projection followed by an immutable Quote projection under the same
|
||||||
|
client command identity; the Store converges both without issuing a second
|
||||||
|
mutation. Evidence is the 33-test Store suite and the cross-repository server task.
|
||||||
|
This is compatible with ADR-007 and needs no separate product-direction decision.
|
||||||
@@ -14,6 +14,7 @@ import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
|||||||
import {
|
import {
|
||||||
designBootstrapFixture,
|
designBootstrapFixture,
|
||||||
designFormFixture,
|
designFormFixture,
|
||||||
|
designQuoteFixture,
|
||||||
designSummaryFixture,
|
designSummaryFixture,
|
||||||
designWorkspaceFixture,
|
designWorkspaceFixture,
|
||||||
} from '../fixtures/design-workspace-v2';
|
} from '../fixtures/design-workspace-v2';
|
||||||
@@ -688,6 +689,135 @@ describe('V2 Living Form store', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the Quote created immediately after a committed generation chat', async () => {
|
||||||
|
const source = await loadedStore();
|
||||||
|
const message = '开始生成';
|
||||||
|
const quoteForm = designFormFixture({
|
||||||
|
directionRevision: 6,
|
||||||
|
rawTurnSequence: 2,
|
||||||
|
workspaceViewRevision: 8,
|
||||||
|
activeQuotes: [designQuoteFixture()],
|
||||||
|
});
|
||||||
|
const canonical = designWorkspaceFixture({
|
||||||
|
form: quoteForm,
|
||||||
|
turns: [
|
||||||
|
...designWorkspaceFixture().turns,
|
||||||
|
{
|
||||||
|
turnId: 'turn-2',
|
||||||
|
rawTurnSequence: 2,
|
||||||
|
userMessage: message,
|
||||||
|
assistantMessage: '我先检查制作方案。准备好后需要你确认一次,确认后才会开始生成。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
fetchProjectMock.mockResolvedValue(canonical);
|
||||||
|
useImageWorkspaceStore.setState({
|
||||||
|
chatDraft: message,
|
||||||
|
pendingOperations: {
|
||||||
|
'operation-1': {
|
||||||
|
id: 'operation-1',
|
||||||
|
label: '发送创作想法',
|
||||||
|
command: {
|
||||||
|
kind: 'apply_input',
|
||||||
|
workspaceId: 'workspace-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
expectedDirectionRevision: 4,
|
||||||
|
clientOperationId: 'operation-1',
|
||||||
|
input: { kind: 'chat', message },
|
||||||
|
},
|
||||||
|
status: 'submitting',
|
||||||
|
error: null,
|
||||||
|
clearDraftPaths: [],
|
||||||
|
clearChatDraft: true,
|
||||||
|
baseRawTurnSequence: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assistantActivities: {
|
||||||
|
'operation-1': {
|
||||||
|
workspaceId: 'workspace-1',
|
||||||
|
directionId: 'direction-1',
|
||||||
|
status: 'submitting',
|
||||||
|
turnId: null,
|
||||||
|
steps: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
source.emit('design.direction.updated', {
|
||||||
|
id: 'session-1:19',
|
||||||
|
type: 'design.direction.updated',
|
||||||
|
clientOperationId: 'operation-1',
|
||||||
|
replayed: false,
|
||||||
|
operation: {
|
||||||
|
operationKind: 'chat',
|
||||||
|
interactionId: 'interaction-2',
|
||||||
|
baseDirectionRevision: 4,
|
||||||
|
newDirectionRevision: 5,
|
||||||
|
rawTurnSequence: 2,
|
||||||
|
specificationRevision: 3,
|
||||||
|
specificationRevisionId: 'specification-revision-3',
|
||||||
|
workspaceViewRevision: 7,
|
||||||
|
specificationRevisionCreated: false,
|
||||||
|
meaningChanged: false,
|
||||||
|
changeSet: { interaction_id: 'interaction-2', changes: [] },
|
||||||
|
turnId: 'turn-2',
|
||||||
|
assistantMessage: '我先检查制作方案。准备好后需要你确认一次,确认后才会开始生成。',
|
||||||
|
createdDecisionPromptIds: [],
|
||||||
|
resolvedDecisionPromptId: null,
|
||||||
|
supersededDecisionPromptIds: [],
|
||||||
|
supersededQuoteCount: 0,
|
||||||
|
quoteId: null,
|
||||||
|
generationTaskId: null,
|
||||||
|
},
|
||||||
|
form: designFormFixture({
|
||||||
|
directionRevision: 5,
|
||||||
|
rawTurnSequence: 2,
|
||||||
|
workspaceViewRevision: 7,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
source.emit('design.direction.updated', {
|
||||||
|
id: 'session-1:20',
|
||||||
|
type: 'design.direction.updated',
|
||||||
|
clientOperationId: 'operation-1',
|
||||||
|
replayed: false,
|
||||||
|
operation: {
|
||||||
|
operationKind: 'quote_request',
|
||||||
|
interactionId: 'interaction-quote-1',
|
||||||
|
baseDirectionRevision: 5,
|
||||||
|
newDirectionRevision: 6,
|
||||||
|
rawTurnSequence: 2,
|
||||||
|
specificationRevision: 3,
|
||||||
|
specificationRevisionId: 'specification-revision-3',
|
||||||
|
workspaceViewRevision: 8,
|
||||||
|
specificationRevisionCreated: false,
|
||||||
|
meaningChanged: false,
|
||||||
|
changeSet: { interaction_id: 'interaction-quote-1', changes: [] },
|
||||||
|
turnId: null,
|
||||||
|
assistantMessage: null,
|
||||||
|
createdDecisionPromptIds: [],
|
||||||
|
resolvedDecisionPromptId: null,
|
||||||
|
supersededDecisionPromptIds: [],
|
||||||
|
supersededQuoteCount: 0,
|
||||||
|
quoteId: 'quote-1',
|
||||||
|
generationTaskId: null,
|
||||||
|
},
|
||||||
|
form: quoteForm,
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(useImageWorkspaceStore.getState()).toMatchObject({
|
||||||
|
chatDraft: '',
|
||||||
|
pendingOperations: {},
|
||||||
|
workspace: { form: { directionRevision: 6 } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(useImageWorkspaceStore.getState().workspace?.form.activeQuotes)
|
||||||
|
.toEqual([designQuoteFixture()]);
|
||||||
|
expect(useImageWorkspaceStore.getState().assistantActivities).toMatchObject({
|
||||||
|
'operation-1': { status: 'completed', turnId: 'turn-2' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps the optimistic chat until a terminal success is visible in the canonical Workspace', async () => {
|
it('keeps the optimistic chat until a terminal success is visible in the canonical Workspace', async () => {
|
||||||
const source = await loadedStore();
|
const source = await loadedStore();
|
||||||
const message = '画一只在月球踢球的熊猫';
|
const message = '画一只在月球踢球的熊猫';
|
||||||
|
|||||||
Reference in New Issue
Block a user