From 7e71a01157d796aca26a76ccf70699f9fef9c945 Mon Sep 17 00:00:00 2001 From: vastsa Date: Wed, 23 Sep 2026 04:00:20 +0800 Subject: [PATCH 1/2] fix(chat): show why a vendor session cannot launch A launch failure before any message is stored left an error-only assistant row that the empty home treated as no transcript, so the chat page went blank. Keep that row visible, and name a vendor-account model the account list did not return. --- apps/desktop/src/components/ChatSurface.tsx | 65 ++++++++++++++------ apps/desktop/src/lib/chat-launch-error.ts | 49 +++++++++++++++ apps/desktop/test/chat-launch-error.test.mjs | 59 ++++++++++++++++++ 3 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src/lib/chat-launch-error.ts create mode 100644 apps/desktop/test/chat-launch-error.test.mjs diff --git a/apps/desktop/src/components/ChatSurface.tsx b/apps/desktop/src/components/ChatSurface.tsx index 70b3e293e5..92ef4476ab 100644 --- a/apps/desktop/src/components/ChatSurface.tsx +++ b/apps/desktop/src/components/ChatSurface.tsx @@ -1,5 +1,9 @@ -import { memo, useMemo } from "react"; +import { memo, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { + messageHasTranscriptContent, + vendorAccountOmitsSessionModel, +} from "../lib/chat-launch-error"; import { Composer } from "./Composer"; import { HomeMascotLogo } from "./HomeMascotLogo"; import { HomeProjectSwitcher } from "./HomeProjectSwitcher"; @@ -53,6 +57,10 @@ export const ChatSurface = memo(function ChatSurface() { const error = useAppStore((state) => state.error); const errorCode = useAppStore((state) => state.errorCode); const errorRetriable = useAppStore((state) => state.errorRetriable); + const [hiddenVendorModelKey, setHiddenVendorModelKey] = useState( + null, + ); + const providers = useAppStore((state) => state.providers); const activeSession = useAppStore((state) => state.activeSessionId ? state.sessions.find((session) => session.id === state.activeSessionId) @@ -107,19 +115,28 @@ export const ChatSurface = memo(function ChatSurface() { const hasTranscript = Boolean(activePermission) || askPending || - messages.some((message) => { - const hasContent = Boolean((message.content || "").trim()); - const hasThinking = - typeof message.thinking === "string" && - Boolean(message.thinking.trim()); - if (message.role === "assistant") return hasContent || hasThinking; - return hasContent || message.role === "tool"; - }); + messages.some((message) => messageHasTranscriptContent(message)); // The empty state belongs to the session on screen. While a cold switch is // still resolving, the visible pane keeps its own transcript, so the hero must // not take over just because the destination projection is still empty. const showEmptyState = !hasTranscript && (!visibleSessionId || visibleSessionId === activeSessionId); + const vendorModelMissing = vendorAccountOmitsSessionModel( + activeSession, + providers, + ); + const vendorModelKey = vendorModelMissing + ? `${activeSession?.providerId ?? ""}:${activeSession?.modelId ?? ""}` + : null; + const showVendorModelError = + vendorModelKey !== null && hiddenVendorModelKey !== vendorModelKey; + const noticeError = + error ?? (showVendorModelError ? t("errors.MODEL_NOT_CONFIGURED") : null); + const noticeCode = error + ? errorCode + : showVendorModelError + ? "MODEL_NOT_CONFIGURED" + : null; return (
)} - {error ? ( + {noticeError ? (
- - {errorCode && i18nHasError(t, errorCode) - ? t(`errors.${errorCode}`) - : error} + + {noticeCode && i18nHasError(t, noticeCode) + ? t(`errors.${noticeCode}`) + : noticeError} + {showVendorModelError && !error && activeSession?.modelId ? ( + <> + {" "} + {activeSession.modelId} + + ) : null} - {(errorCode === "MODEL_NOT_CONFIGURED" || - errorCode === "PROVIDER_SECRET_MISSING" || - errorCode === "PROVIDER_UNAUTHORIZED") && ( + {(noticeCode === "MODEL_NOT_CONFIGURED" || + noticeCode === "PROVIDER_SECRET_MISSING" || + noticeCode === "PROVIDER_UNAUTHORIZED") && (