From 0eaf787e8442e4ed34143a7b28ec50e06458fa79 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 27 Nov 2025 18:59:09 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20use=20ResultSchema=20for?= =?UTF-8?q?=20sendMessage=20output=20to=20prevent=20field=20stripping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zod's z.union() tries schemas in order and returns the first match. The sendMessage output had ResultSchema(z.void()) first, which matched {success: true} and stripped workspaceId/metadata as "extra keys" before the workspace creation schema could match. Fixed by using a single ResultSchema with optional workspaceId/metadata fields instead of a union, ensuring consistent response shape. Change-Id: Id55f726d7814d75f03169dfd5fa35bd0e4c38886 Signed-off-by: Thomas Kosiewski --- .../ChatInput/useCreationWorkspace.test.tsx | 14 ++++++++----- .../ChatInput/useCreationWorkspace.ts | 9 ++++---- src/common/orpc/schemas/api.ts | 11 +++++----- src/node/orpc/router.ts | 21 +++++++------------ src/node/services/workspaceService.ts | 1 + tests/integration/helpers.ts | 6 +++--- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/browser/components/ChatInput/useCreationWorkspace.test.tsx b/src/browser/components/ChatInput/useCreationWorkspace.test.tsx index 4cb0f1d69..5df37abdf 100644 --- a/src/browser/components/ChatInput/useCreationWorkspace.test.tsx +++ b/src/browser/components/ChatInput/useCreationWorkspace.test.tsx @@ -138,14 +138,16 @@ const setupWindow = ({ listBranches, sendMessage }: SetupWindowOptions = {}) => if (!args.workspaceId) { return Promise.resolve({ success: true, - workspaceId: TEST_WORKSPACE_ID, - metadata: TEST_METADATA, + data: { + workspaceId: TEST_WORKSPACE_ID, + metadata: TEST_METADATA, + }, } satisfies WorkspaceSendMessageResult); } const existingWorkspaceResult: WorkspaceSendMessageResult = { success: true, - data: undefined, + data: {}, }; return Promise.resolve(existingWorkspaceResult); }); @@ -357,8 +359,10 @@ describe("useCreationWorkspace", () => { (_args: WorkspaceSendMessageArgs): Promise => Promise.resolve({ success: true as const, - workspaceId: TEST_WORKSPACE_ID, - metadata: TEST_METADATA, + data: { + workspaceId: TEST_WORKSPACE_ID, + metadata: TEST_METADATA, + }, }) ); const { workspaceApi } = setupWindow({ diff --git a/src/browser/components/ChatInput/useCreationWorkspace.ts b/src/browser/components/ChatInput/useCreationWorkspace.ts index d9ab388ff..a6c5217ff 100644 --- a/src/browser/components/ChatInput/useCreationWorkspace.ts +++ b/src/browser/components/ChatInput/useCreationWorkspace.ts @@ -128,16 +128,17 @@ export function useCreationWorkspace({ return false; } - // Check if this is a workspace creation result (has metadata field) - if ("metadata" in result && result.metadata) { - syncCreationPreferences(projectPath, result.metadata.id); + // Check if this is a workspace creation result (has metadata in data) + const { metadata } = result.data; + if (metadata) { + syncCreationPreferences(projectPath, metadata.id); if (projectPath) { const pendingInputKey = getInputKey(getPendingScopeId(projectPath)); updatePersistedState(pendingInputKey, ""); } // Settings are already persisted via useDraftWorkspaceSettings // Notify parent to switch workspace (clears input via parent unmount) - onWorkspaceCreated(result.metadata); + onWorkspaceCreated(metadata); setIsSending(false); return true; } else { diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index d6260fa9f..51a395500 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -167,14 +167,13 @@ export const workspace = { trunkBranch: z.string().optional(), }).optional(), }), - output: z.union([ - ResultSchema(z.void(), SendMessageErrorSchema), + output: ResultSchema( z.object({ - success: z.literal(true), - workspaceId: z.string(), - metadata: FrontendWorkspaceMetadataSchema, + workspaceId: z.string().optional(), + metadata: FrontendWorkspaceMetadataSchema.optional(), }), - ]), + SendMessageErrorSchema + ), }, resumeStream: { input: z.object({ diff --git a/src/node/orpc/router.ts b/src/node/orpc/router.ts index 5df9cb8b2..5c1c7e997 100644 --- a/src/node/orpc/router.ts +++ b/src/node/orpc/router.ts @@ -199,12 +199,9 @@ export const router = (authToken?: string) => { const result = await context.workspaceService.sendMessage( input.workspaceId, input.message, - input.options // Cast to avoid Zod vs Interface mismatch + input.options ); - // Type mismatch handling: WorkspaceService returns Result. - // Our schema output is ResultSchema(void, SendMessageError) OR {success:true, ...} - // We need to ensure strict type alignment. if (!result.success) { const error = typeof result.error === "string" @@ -212,22 +209,20 @@ export const router = (authToken?: string) => { : result.error; return { success: false, error }; } - // If success, it returns different shapes depending on lazy creation. - // If lazy creation happened, it returns {success: true, workspaceId, metadata} - // If regular message, it returns {success: true, data: ...} -> wait, result.data is undefined for normal message? - // SendMessage in AgentSession returns Result mostly. - // But createForFirstMessage returns object. - // Check result shape + // Check if this is a workspace creation result if ("workspaceId" in result) { return { success: true, - workspaceId: result.workspaceId, - metadata: result.metadata, + data: { + workspaceId: result.workspaceId, + metadata: result.metadata, + }, }; } - return { success: true, data: undefined }; + // Regular message send (no workspace creation) + return { success: true, data: {} }; }), resumeStream: t .input(schemas.workspace.resumeStream.input) diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index dde8f221a..6839855cc 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -453,6 +453,7 @@ export class WorkspaceService extends EventEmitter { const allMetadata = await this.config.getAllWorkspaceMetadata(); const completeMetadata = allMetadata.find((m) => m.id === workspaceId); + if (!completeMetadata) { return { success: false, error: "Failed to retrieve workspace metadata" }; } diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index a11613253..59ab50dd0 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -132,12 +132,12 @@ export async function sendMessage( return { success: false, error: { type: "unknown", raw } }; } - if (result.success && "workspaceId" in result) { - // Lazy workspace creation path returns metadata/workspaceId; normalize to void success for callers + // Normalize to Result for callers - they just care about success/failure + if (result.success) { return { success: true, data: undefined }; } - return result; + return { success: false, error: result.error }; } /**