From 58f0a312ef6b3fad799494cc10830061b2670926 Mon Sep 17 00:00:00 2001 From: chenchen Date: Sat, 9 May 2026 16:10:58 +0800 Subject: [PATCH] fix(client): suppress run/stop button flicker during CLI restart transitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ModelSelector and PermissionMode changes trigger a server-side CLI restart (handler.ts:521 restartSessionWithRuntimeConfig). The server emits status:'thinking' with verb 'Switching provider and model...' or 'Restarting session...' to keep the spinner spinning during the ~3s restart window. ChatInput's isActive=`chatState !== 'idle' && hasMessages` guard let the run button flash to a red 「stop」 affordance during that window for any session with prior messages — confusing because hitting stop has nothing to interrupt. Add a statusVerb prefix check so isActive stays false during these system transitions: isSystemRestartTransition = statusVerb.startsWith('Switching provider and model') || statusVerb.startsWith('Restarting session') isActive = chatState !== 'idle' && hasMessages && !isSystemRestartTransition Match by prefix so any future suffix (e.g. " (CLI starting...)") still trips the guard. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../desktop/src/components/chat/ChatInput.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/cc-haha/desktop/src/components/chat/ChatInput.tsx b/cc-haha/desktop/src/components/chat/ChatInput.tsx index eae19ad..7dbf3a4 100644 --- a/cc-haha/desktop/src/components/chat/ChatInput.tsx +++ b/cc-haha/desktop/src/components/chat/ChatInput.tsx @@ -61,6 +61,7 @@ export function ChatInput({ variant = 'default' }: ChatInputProps) { const activeTabId = useTabStore((s) => s.activeTabId) const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined) const chatState = sessionState?.chatState ?? 'idle' + const statusVerb = sessionState?.statusVerb ?? '' const slashCommands = sessionState?.slashCommands ?? [] const composerPrefill = sessionState?.composerPrefill ?? null const activeSession = useSessionStore((state) => activeTabId ? state.sessions.find((session) => session.id === activeTabId) ?? null : null) @@ -69,12 +70,19 @@ export function ChatInput({ variant = 'default' }: ChatInputProps) { const hasMessages = useChatStore((s) => activeTabId ? (s.sessions[activeTabId]?.messages?.length ?? 0) > 0 : false) const isMemberSession = !!memberInfo - // Bug fix: ModelSelector / runtime config changes briefly flip - // chatState off-idle (CLI reconnect / startup), which used to make the - // run button render as a red 「stop」 even on an empty session. Gate by - // hasMessages so 「stop」 only appears once we actually have something - // running for the user to stop. - const isActive = chatState !== 'idle' && hasMessages + // Bug fix: ModelSelector / permission-mode changes trigger a server-side + // CLI restart that briefly flips chatState off-idle with one of the + // verbs below. There is no inference to stop during that window — the + // user just has to wait a couple seconds — so don't render the red + // 「stop」 button regardless of hasMessages. Match the verbs as + // prefixes since server may append context (e.g. " (CLI starting...)"). + const isSystemRestartTransition = + statusVerb.startsWith('Switching provider and model') || + statusVerb.startsWith('Restarting session') + // Also: empty sessions can't be active for the user, even if chatState + // briefly flips during initial CLI startup. + const isActive = + chatState !== 'idle' && hasMessages && !isSystemRestartTransition const isWorkspaceMissing = activeSession?.workDirExists === false const canSubmit = !isWorkspaceMissing && (input.trim().length > 0 || (!isMemberSession && attachments.length > 0)) const isHeroComposer = variant === 'hero' && !isMemberSession