fix(client): suppress run/stop button flicker during CLI restart transitions

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 16:10:58 +08:00
co-authored by Claude Opus 4.7
parent 30056136ea
commit 58f0a312ef
@@ -61,6 +61,7 @@ export function ChatInput({ variant = 'default' }: ChatInputProps) {
const activeTabId = useTabStore((s) => s.activeTabId) const activeTabId = useTabStore((s) => s.activeTabId)
const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined) const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined)
const chatState = sessionState?.chatState ?? 'idle' const chatState = sessionState?.chatState ?? 'idle'
const statusVerb = sessionState?.statusVerb ?? ''
const slashCommands = sessionState?.slashCommands ?? [] const slashCommands = sessionState?.slashCommands ?? []
const composerPrefill = sessionState?.composerPrefill ?? null const composerPrefill = sessionState?.composerPrefill ?? null
const activeSession = useSessionStore((state) => activeTabId ? state.sessions.find((session) => session.id === activeTabId) ?? null : 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 hasMessages = useChatStore((s) => activeTabId ? (s.sessions[activeTabId]?.messages?.length ?? 0) > 0 : false)
const isMemberSession = !!memberInfo const isMemberSession = !!memberInfo
// Bug fix: ModelSelector / runtime config changes briefly flip // Bug fix: ModelSelector / permission-mode changes trigger a server-side
// chatState off-idle (CLI reconnect / startup), which used to make the // CLI restart that briefly flips chatState off-idle with one of the
// run button render as a red 「stop」 even on an empty session. Gate by // verbs below. There is no inference to stop during that window — the
// hasMessages so 「stop」 only appears once we actually have something // user just has to wait a couple seconds — so don't render the red
// running for the user to stop. // 「stop」 button regardless of hasMessages. Match the verbs as
const isActive = chatState !== 'idle' && hasMessages // 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 isWorkspaceMissing = activeSession?.workDirExists === false
const canSubmit = !isWorkspaceMissing && (input.trim().length > 0 || (!isMemberSession && attachments.length > 0)) const canSubmit = !isWorkspaceMissing && (input.trim().length > 0 || (!isMemberSession && attachments.length > 0))
const isHeroComposer = variant === 'hero' && !isMemberSession const isHeroComposer = variant === 'hero' && !isMemberSession