Skip to content

Commit aa99287

Browse files
committed
fix(desktop): wait for cancel IPC ack + version generate payload
Major #1: cancelGeneration now waits for the IPC round-trip before clearing isGenerating/activeGenerationId. If main rejects the cancel (bad id, already aborted) the error becomes visible via toast instead of being silently swallowed. Updated store test to reflect async cancel and drain microtasks between cancel and the follow-up sendPrompt. Major #2: add GeneratePayloadV1 (schemaVersion: 1, generationId required) to @open-codesign/shared and rename the IPC channel to codesign:v1:generate, matching the pattern established by codesign:v1:cancel-generation. Preload builds the v1 envelope with satisfies check; main validates with GeneratePayloadV1.parse(). Add 5 schema tests for missing/wrong schemaVersion and empty generationId. Add notifications.cancellationFailed i18n key. Signed-off-by: hqhq1025 <1506751656@qq.com>
1 parent 162e637 commit aa99287

8 files changed

Lines changed: 86 additions & 11 deletions

File tree

‎apps/desktop/src/main/index.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
BRAND,
99
CancelGenerationPayloadV1,
1010
CodesignError,
11-
GeneratePayload,
11+
GeneratePayloadV1,
1212
} from '@open-codesign/shared';
1313
import type { BrowserWindow as ElectronBrowserWindow } from 'electron';
1414
import { autoUpdater } from 'electron-updater';
@@ -129,10 +129,10 @@ function registerIpcHandlers(): void {
129129
return nextState;
130130
});
131131

132-
ipcMain.handle('codesign:generate', async (_e, raw: unknown) => {
133-
const payload = GeneratePayload.parse(raw);
132+
ipcMain.handle('codesign:v1:generate', async (_e, raw: unknown) => {
133+
const payload = GeneratePayloadV1.parse(raw);
134134
const controller = new AbortController();
135-
const id = payload.generationId ?? `gen-${Date.now()}`;
135+
const id = payload.generationId;
136136
inFlight.set(id, controller);
137137

138138
const apiKey = getApiKeyForProvider(payload.model.provider);

‎apps/desktop/src/preload/index.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
CancelGenerationPayloadV1,
33
ChatMessage,
4+
GeneratePayloadV1,
45
LocalInputFile,
56
ModelRef,
67
OnboardingState,
@@ -35,9 +36,13 @@ const api = {
3536
model: ModelRef;
3637
baseUrl?: string;
3738
referenceUrl?: string;
38-
attachments?: LocalInputFile[];
39-
generationId?: string;
40-
}) => ipcRenderer.invoke('codesign:generate', payload),
39+
attachments: LocalInputFile[];
40+
generationId: string;
41+
}) =>
42+
ipcRenderer.invoke('codesign:v1:generate', {
43+
schemaVersion: 1,
44+
...payload,
45+
} satisfies GeneratePayloadV1),
4146
cancelGeneration: (generationId: string) =>
4247
ipcRenderer.invoke('codesign:v1:cancel-generation', {
4348
schemaVersion: 1,

‎apps/desktop/src/renderer/src/store.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('useCodesignStore generation cancellation', () => {
5151
string,
5252
ReturnType<typeof deferred<{ artifacts: Array<{ content: string }>; message: string }>>
5353
>();
54-
const cancelGeneration = vi.fn();
54+
const cancelGeneration = vi.fn(() => Promise.resolve());
5555
const generate = vi.fn((payload: { generationId?: string }) => {
5656
if (!payload.generationId) throw new Error('missing generationId');
5757
const task = deferred<{ artifacts: Array<{ content: string }>; message: string }>();
@@ -73,6 +73,9 @@ describe('useCodesignStore generation cancellation', () => {
7373

7474
useCodesignStore.getState().cancelGeneration();
7575

76+
// Drain microtasks so the cancel IPC promise resolves and clears state
77+
await Promise.resolve();
78+
7679
const secondRun = useCodesignStore.getState().sendPrompt({ prompt: 'second prompt' });
7780
const secondId = useCodesignStore.getState().activeGenerationId;
7881
if (!secondId) throw new Error('expected second generation id');
@@ -117,7 +120,7 @@ describe('useCodesignStore generation cancellation', () => {
117120
vi.stubGlobal('window', {
118121
codesign: {
119122
generate,
120-
cancelGeneration: vi.fn(),
123+
cancelGeneration: vi.fn(() => Promise.resolve()),
121124
},
122125
setTimeout,
123126
});

‎apps/desktop/src/renderer/src/store.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,21 @@ export const useCodesignStore = create<CodesignState>((set, get) => ({
365365
cancelGeneration() {
366366
const id = get().activeGenerationId;
367367
if (!id || !window.codesign) return;
368-
window.codesign.cancelGeneration(id);
369-
set({ isGenerating: false, activeGenerationId: null });
368+
369+
void window.codesign
370+
.cancelGeneration(id)
371+
.then(() => {
372+
finishIfCurrent(set, id, () => ({ isGenerating: false, activeGenerationId: null }));
373+
})
374+
.catch((err) => {
375+
const msg = err instanceof Error ? err.message : tr('errors.unknown');
376+
set({ errorMessage: msg, lastError: msg });
377+
get().pushToast({
378+
variant: 'error',
379+
title: tr('notifications.cancellationFailed'),
380+
description: msg,
381+
});
382+
});
370383
},
371384

372385
async retryLastPrompt() {

‎packages/i18n/src/locales/en.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
"designSystemCleared": "Design system cleared",
166166
"clearDesignSystemFailed": "Unable to clear design system",
167167
"generationFailed": "Generation failed",
168+
"cancellationFailed": "Cancellation failed",
168169
"inlineCommentFailed": "Inline comment failed",
169170
"noDesignToExport": "No design to export yet.",
170171
"exportedTo": "Exported to {{path}}"

‎packages/i18n/src/locales/zh-CN.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
"designSystemCleared": "设计系统已清除",
166166
"clearDesignSystemFailed": "无法清除设计系统",
167167
"generationFailed": "生成失败",
168+
"cancellationFailed": "取消失败",
168169
"inlineCommentFailed": "局部修改失败",
169170
"noDesignToExport": "当前还没有可导出的设计。",
170171
"exportedTo": "已导出到 {{path}}"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { GeneratePayloadV1 } from './index';
3+
4+
const BASE_VALID = {
5+
schemaVersion: 1 as const,
6+
prompt: 'Design a landing page',
7+
history: [],
8+
model: { provider: 'anthropic' as const, modelId: 'claude-sonnet-4-6' },
9+
generationId: 'gen-abc123',
10+
};
11+
12+
describe('GeneratePayloadV1', () => {
13+
it('accepts a valid v1 payload', () => {
14+
const result = GeneratePayloadV1.parse(BASE_VALID);
15+
expect(result.schemaVersion).toBe(1);
16+
expect(result.generationId).toBe('gen-abc123');
17+
expect(result.attachments).toEqual([]);
18+
});
19+
20+
it('rejects a payload missing schemaVersion', () => {
21+
const { schemaVersion: _, ...noVersion } = BASE_VALID;
22+
expect(() => GeneratePayloadV1.parse(noVersion)).toThrow();
23+
});
24+
25+
it('rejects a payload with a future schemaVersion (forward incompat)', () => {
26+
expect(() => GeneratePayloadV1.parse({ ...BASE_VALID, schemaVersion: 2 })).toThrow();
27+
});
28+
29+
it('rejects a payload with an empty generationId', () => {
30+
expect(() => GeneratePayloadV1.parse({ ...BASE_VALID, generationId: '' })).toThrow();
31+
});
32+
33+
it('rejects a payload missing generationId', () => {
34+
const { generationId: _, ...noId } = BASE_VALID;
35+
expect(() => GeneratePayloadV1.parse(noId)).toThrow();
36+
});
37+
});

‎packages/shared/src/index.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ export const GeneratePayload = z.object({
114114
});
115115
export type GeneratePayload = z.infer<typeof GeneratePayload>;
116116

117+
/** @deprecated Use GeneratePayloadV1. */
118+
export type LegacyGeneratePayload = GeneratePayload;
119+
120+
export const GeneratePayloadV1 = z.object({
121+
schemaVersion: z.literal(1),
122+
prompt: z.string().min(1).max(32_000),
123+
history: z.array(ChatMessage).max(200),
124+
model: ModelRef,
125+
baseUrl: z.string().url().optional(),
126+
referenceUrl: z.string().url().optional(),
127+
attachments: z.array(LocalInputFile).max(12).default([]),
128+
generationId: z.string().min(1),
129+
});
130+
export type GeneratePayloadV1 = z.infer<typeof GeneratePayloadV1>;
131+
117132
export const ApplyCommentPayload = z.object({
118133
html: z.string().min(1).max(500_000),
119134
comment: z.string().min(1).max(4_000),

0 commit comments

Comments
 (0)