From 06e6f395030f5a35fb97255184ec1af21281bd98 Mon Sep 17 00:00:00 2001 From: BlitzOS Upstream Prep Date: Wed, 2 Sep 2026 00:25:16 +0000 Subject: [PATCH] fix(components): a missing cloud token must not disable the local file handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three attachment entry points bail on `if (!workspaceId || !authToken)` before they reach the local handoff, and that handoff needs no cloud token: it hands the bytes to the machine that runs the session over the local IPC transport. A composition with no cloud token — the local desktop entry, which must not make authenticated product-cloud requests — therefore fails every `+` attachment with "Missing workspace or auth token" without issuing a single request, and Retry re-enters the same guard. Move the local handoff in front of the guard in `startFileUpload` and in the chat-landing file draft, adding `workspaceId &&` to its own condition; the guard keeps its text and still owns the cloud path below it. For images, widen the guard to `!workspaceId || (!authToken && !canSendFileLocally)` and throw inside the `try` when there is no token, so a tokenless image lands in the `catch` that already degrades a failed image upload to a pending file attachment over the local transport. With a token present the order of operations is unchanged: the guard passes, the local path runs first and the cloud path second, exactly as before. Model: claude-opus-5[1m] --- .../sessions/session-chat-input-area.tsx | 38 +++++++++++++------ .../src/hooks/use-chat-landing-file-draft.ts | 26 +++++++------ 2 files changed, 41 insertions(+), 23 deletions(-) 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,