diff --git a/packages/components/src/components/sessions/session-chat-input-area.tsx b/packages/components/src/components/sessions/session-chat-input-area.tsx index 41bbbd12a..dcd226cec 100644 --- a/packages/components/src/components/sessions/session-chat-input-area.tsx +++ b/packages/components/src/components/sessions/session-chat-input-area.tsx @@ -906,7 +906,11 @@ export const SessionChatInputArea = memo( const startUpload = useCallback( async (targetSessionId: SessionId, localId: string, file: File) => { - if (!workspaceId || !authToken) { + // Without a token there is no cloud upload to attempt — but the local + // transport can still take the bytes, and the `catch` below already + // degrades an image to a pending file over it. So fail here only when + // that path is unavailable too. + if (!workspaceId || (!authToken && !canSendFileLocally)) { capturePostHogEvent(postHog, 'session/image_upload_failed', { channel: 'web', entrypoint: 'session_chat', @@ -952,6 +956,12 @@ export const SessionChatInputArea = memo( const uploadStartedAtMs = getPerformanceNowMs(); try { + if (!authToken) { + // The guard above let this through because the local transport can + // serve it. Take the degrade-to-file path in the `catch`, which is + // where an unavailable image upload is already handled. + throw new Error(imageUploadMissingAuthLabel); + } const uploaded = await uploadSessionImage({ workspaceId, sessionId: targetSessionId, @@ -1087,23 +1097,17 @@ export const SessionChatInputArea = memo( const startFileUpload = useCallback( async (targetSessionId: SessionId, localId: string, file: File) => { - if (!workspaceId || !authToken) { - updatePendingFile(targetSessionId, localId, (entry) => ({ - ...entry, - status: 'failed', - progress: 0, - error: fileUploadMissingAuthLabel, - })); - return; - } - // Desktop local-transport fast path: hand bytes straight to the local CLI // (zero relay round trip). The CLI stores the blob and returns a // transport:'local' block, which we drop into `uploaded` exactly like a // cloud upload — the block then rides the outgoing message via // toFileInputBlock. No progress bar: the handoff completes in one step. // On any failure we fall through to the cloud path below. - if (canSendFileLocally && session.machineId) { + // + // It runs BEFORE the cloud-credential guard, because the handoff needs no + // cloud token: a local-only composition has none, and would otherwise fail + // every attachment at a check for credentials it never uses. + if (canSendFileLocally && workspaceId && session.machineId) { try { const outcome = await sendSessionFileToLocalRuntime({ workspaceId, @@ -1127,6 +1131,16 @@ export const SessionChatInputArea = memo( } } + if (!workspaceId || !authToken) { + updatePendingFile(targetSessionId, localId, (entry) => ({ + ...entry, + status: 'failed', + progress: 0, + error: fileUploadMissingAuthLabel, + })); + return; + } + const abort = new AbortController(); updatePendingFile(targetSessionId, localId, (entry) => ({ ...entry, diff --git a/packages/components/src/hooks/use-chat-landing-file-draft.ts b/packages/components/src/hooks/use-chat-landing-file-draft.ts index bc6a26ab3..a62804b3e 100644 --- a/packages/components/src/hooks/use-chat-landing-file-draft.ts +++ b/packages/components/src/hooks/use-chat-landing-file-draft.ts @@ -140,21 +140,15 @@ export function useChatLandingFileDraft(args: { const startUpload = useCallback( async (localId: string, file: File, sessionId: SessionId) => { - if (!workspaceId || !authToken) { - updatePendingFile(localId, (entry) => ({ - ...entry, - status: 'failed', - progress: 0, - error: fileUploadMissingAuthLabel, - })); - return; - } - // Desktop local-transport fast path: hand bytes straight to the local CLI // (zero relay round trip). The CLI returns a transport:'local' block that // drops into `uploaded` exactly like a cloud upload. On any failure we fall // through to the cloud path below. - if (canSendFileLocally && machineId) { + // + // It runs BEFORE the cloud-credential guard, because the handoff needs no + // cloud token: a local-only composition has none, and would otherwise fail + // every attachment at a check for credentials it never uses. + if (canSendFileLocally && workspaceId && machineId) { try { const outcome = await sendSessionFileToLocalRuntime({ workspaceId, @@ -178,6 +172,16 @@ export function useChatLandingFileDraft(args: { } } + if (!workspaceId || !authToken) { + updatePendingFile(localId, (entry) => ({ + ...entry, + status: 'failed', + progress: 0, + error: fileUploadMissingAuthLabel, + })); + return; + } + const abort = new AbortController(); updatePendingFile(localId, (entry) => ({ ...entry,