mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* feat(subscription): add development login, models and web search * feat(subscription): refine account flows and host integration Align desktop and VS Code subscription UI, add account logout and confirmed plan display, and map subscription thinking parameters. Remove simulation code and obsolete native authentication paths, consolidate design documentation, and cover cancellation and recovery boundaries. * fix(subscription): preserve request errors and remove unused compatibility * fix(subscription): refine command visibility and client logout flows * fix(subscription): simplify account actions and desktop sign-in feedback * docs(subscription): clarify sign-in and provider guidance * feat(subscription): support switching accounts across clients * fix(subscription): align account actions and open plans on demand * fix(subscription): restore model selection after logout Filter subscription-only commands consistently across terminal renderers, preserve model catalog error classification, and display the subscription authentication name consistently. * fix(subscription): unify status authentication labels across clients * fix(subscription): preserve provider connections across client logout Restore configured providers after subscription logout across CLI, Desktop, and VS Code. Keep unrelated connections and conversation history, synchronize model state silently, and return to an unconfigured chat when no provider remains. * fix(subscription): correct native client logout state * feat(subscription): enable sign-in without development mode * refactor(ui): 优化会员登录成功提示和界面交互体验 - 移除订阅欢迎提示相关状态和行为,简化 UI 状态管理 - 替换登录成功提示为四行摘要,包含邮箱、套餐、模型及常用命令提示 - 更新国际化文本,支持新的登录成功摘要内容 - 取消订阅模式下输入框下方的命令提示行,改为在登录成功摘要中展示 - 移除多余的测试用例,新增登录成功消息的单元测试校验 - 优化 AppHeader 组件,不再显示订阅启动重复提示 - 调整 Composer 组件逻辑,删除订阅提示相关副作用及渲染条件 - 修改历史消息显示组件以适应新的成功提示格式 * fix(subscription): enable Flash reasoning controls in shared UI * fix(subscription): prioritize authorized models and simplify login dialogs * fix(subscription): preserve gateway errors and request IDs * fix(subscription): synchronize account state across clients Preserve model selections after sign-out, synchronize account commands and model catalogs, and retain explicit browser sign-in flows across CLI, Web Shell, and VS Code. Cancel subscription background agents on logout and normalize expired refresh credentials without SDK error wrapping. * fix(subscription): reset default model on explicit sign-in Choose the default served model when explicitly signing in to Qwen Subscription, preferring Max over a previously saved Flash selection. Preserve normal startup behavior and other provider flows, and cover CLI and workspace activation with regression tests. * fix(vscode): refresh models after successful sign-in * fix(web-shell): hide account actions during automatic sign-in * fix(web-shell): align model details and auth keyboard navigation * docs(subscription): revert previous guide changes * fix(core): reuse subscription catalog for same-login model switches * docs(subscription): document Qwen membership sign-in * fix(subscription): unify quota notices and preserve provider switching Carry structured subscription quota details through chat and search errors for localized CLI, Desktop, and VS Code display. Keep other providers available when subscription discovery fails and refine sidebar account actions. * fix(web-shell): align Chinese subscription entry title with CLI * fix(subscription): open plans for saved non-member accounts * fix(web-shell): show subscription commands after desktop sign-in * fix(subscription): reauthorize explicit account sign-ins * fix(auth): add model switching hint to subscription success * docs(auth): list Qwen OAuth as the first option * docs(models): simplify subscription sign-in guidance * docs(models): remove legacy OAuth from supported auth types * docs(subscription): link membership references and remove obsolete allowance note * fix(subscription): preserve session until reauthorization succeeds * fix(subscription): use pre for browser authorization and plans * fix(subscription): use production endpoints * test(vscode): fix editor message test formatting * fix(subscription): preserve unauthenticated session startup * fix(cli): classify subscription auth during ACP preflight * fix(subscription): scope activation to the current project --------- Co-authored-by: luoyun <[email protected]>
190 lines
7.8 KiB
TypeScript
190 lines
7.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { Box, Text, useIsScreenReaderEnabled, type DOMElement } from 'ink';
|
|
import { useCallback, useRef, useState, type RefObject } from 'react';
|
|
import { LoadingIndicator } from './LoadingIndicator.js';
|
|
import { InputPrompt } from './InputPrompt.js';
|
|
import { Footer } from './Footer.js';
|
|
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
|
import { KeyboardShortcuts } from './KeyboardShortcuts.js';
|
|
import { useUIState } from '../contexts/UIStateContext.js';
|
|
import { useUIActions } from '../contexts/UIActionsContext.js';
|
|
import { useVimModeState } from '../contexts/VimModeContext.js';
|
|
import { useConfig } from '../contexts/ConfigContext.js';
|
|
import { theme } from '../semantic-colors.js';
|
|
import { StreamingState } from '../types.js';
|
|
import { FeedbackDialog } from '../FeedbackDialog.js';
|
|
import { AuthType } from '@qwen-code/qwen-code-core/utils/auth-type.js';
|
|
import { useModBand } from '../hooks/use-mod-band.js';
|
|
import { ModBand } from './ModBand.js';
|
|
import { modBandMaxRows } from '../utils/mod-band-tree.js';
|
|
import { t } from '../../i18n/index.js';
|
|
|
|
interface ComposerProps {
|
|
footerRef?: RefObject<DOMElement | null>;
|
|
}
|
|
|
|
export const Composer = ({ footerRef }: ComposerProps) => {
|
|
const config = useConfig();
|
|
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
const uiState = useUIState();
|
|
const subscriptionActive =
|
|
config.getAuthType() === AuthType.QWEN_SUBSCRIPTION;
|
|
const uiActions = useUIActions();
|
|
const { vimEnabled } = useVimModeState();
|
|
const modBand = useModBand(config);
|
|
|
|
const {
|
|
showAutoAcceptIndicator,
|
|
streamingResponseLengthRef,
|
|
isReceivingContent,
|
|
responseCandidateTokens,
|
|
taskStartTokens,
|
|
taskStartStreamingChars,
|
|
} = uiState;
|
|
|
|
// Real-time token animation is performed inside LoadingIndicator itself, so
|
|
// the 100ms polling only re-renders that one component — keeping InputPrompt
|
|
// and Footer static avoids terminal flicker during streaming.
|
|
const isStreaming =
|
|
uiState.streamingState === StreamingState.Responding ||
|
|
uiState.streamingState === StreamingState.WaitingForConfirmation;
|
|
// `isStreaming` covers Responding|WaitingForConfirmation, but we only
|
|
// suppress during Responding (active token output). A confirmation prompt
|
|
// must remain visible regardless of width. Drop the redundant `isStreaming`
|
|
// guard so future expansions of `isStreaming` don't silently widen suppression.
|
|
const suppressBottomLoadingIndicator =
|
|
uiState.streamingState === StreamingState.Responding &&
|
|
uiState.terminalWidth <= 30;
|
|
|
|
// State for keyboard shortcuts display toggle
|
|
const [showShortcuts, setShowShortcuts] = useState(false);
|
|
const handleToggleShortcuts = useCallback(() => {
|
|
setShowShortcuts((prev) => !prev);
|
|
}, []);
|
|
|
|
// State for autocomplete-dropdown visibility (narrow signal). Drives the
|
|
// Footer / KeyboardShortcuts hide-when-dropdown-visible logic below; kept
|
|
// local to Composer because nothing outside this component needs the
|
|
// narrow signal.
|
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
const clipboardUnavailableShownRef = useRef(false);
|
|
|
|
// Broad signal — any input-area Tab consumer. Forwarded to AppContainer
|
|
// via UIActionsContext so useAutoAcceptIndicator's `shouldBlockTab` can
|
|
// suppress the Windows-only bare-Tab approval-mode fallback. See #4171.
|
|
const handleTabConsumerChange = useCallback(
|
|
(active: boolean) => {
|
|
uiActions.onTabConsumerChange(active);
|
|
},
|
|
[uiActions],
|
|
);
|
|
|
|
return (
|
|
<Box flexDirection="column" marginTop={1}>
|
|
{!uiState.embeddedShellFocused && !suppressBottomLoadingIndicator && (
|
|
<LoadingIndicator
|
|
// Hide loading phrases when enableLoadingPhrases is explicitly false.
|
|
// Using === false ensures phrases show by default when undefined.
|
|
currentLoadingPhrase={
|
|
config.getAccessibility()?.enableLoadingPhrases === false
|
|
? undefined
|
|
: uiState.currentLoadingPhrase
|
|
}
|
|
elapsedTime={uiState.elapsedTime}
|
|
candidatesTokens={responseCandidateTokens}
|
|
taskStartTokens={taskStartTokens}
|
|
taskStartStreamingChars={taskStartStreamingChars}
|
|
streamingCharsRef={streamingResponseLengthRef}
|
|
isStreaming={isStreaming}
|
|
showResponseTokensPerSecond={config.getShowResponseTokensPerSecond()}
|
|
isReceivingContent={isReceivingContent}
|
|
/>
|
|
)}
|
|
{/*
|
|
* Narrow-terminal fallback: when the full LoadingIndicator is suppressed
|
|
* (≤30 cols, actively Responding) we still surface a minimal `esc to
|
|
* cancel` hint so users on ultra-narrow terminals retain the cancel
|
|
* affordance during long-running calls. The full timer/spinner/phrase
|
|
* UI is still suppressed to avoid layout breakage.
|
|
*/}
|
|
{!uiState.embeddedShellFocused && suppressBottomLoadingIndicator && (
|
|
<Box paddingLeft={2}>
|
|
<Text color={theme.text.secondary}>({t('Esc to cancel')})</Text>
|
|
</Box>
|
|
)}
|
|
|
|
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
|
|
|
|
{uiState.isFeedbackDialogOpen && <FeedbackDialog />}
|
|
|
|
{uiState.isInputActive && (
|
|
<ModBand
|
|
band={modBand}
|
|
maxRows={modBandMaxRows(uiState.terminalHeight)}
|
|
/>
|
|
)}
|
|
|
|
{uiState.isInputActive && (
|
|
<InputPrompt
|
|
buffer={uiState.buffer}
|
|
pendingPastes={uiState.pendingPastes}
|
|
onPendingPastesChange={uiActions.setPendingPastes}
|
|
inputWidth={uiState.inputWidth}
|
|
suggestionsWidth={uiState.suggestionsWidth}
|
|
onSubmit={uiActions.handleFinalSubmit}
|
|
userMessages={uiState.userMessages}
|
|
onClearScreen={uiActions.handleClearScreen}
|
|
config={config}
|
|
slashCommands={uiState.slashCommands}
|
|
commandContext={uiState.commandContext}
|
|
recentSlashCommands={uiState.recentSlashCommands}
|
|
shellModeActive={uiState.shellModeActive}
|
|
setShellModeActive={uiActions.setShellModeActive}
|
|
approvalMode={showAutoAcceptIndicator}
|
|
onEscapePromptChange={uiActions.onEscapePromptChange}
|
|
onToggleShortcuts={handleToggleShortcuts}
|
|
showShortcuts={showShortcuts}
|
|
onSuggestionsVisibilityChange={setShowSuggestions}
|
|
onTabConsumerChange={handleTabConsumerChange}
|
|
focus={true}
|
|
vimHandleInput={uiActions.vimHandleInput}
|
|
isEmbeddedShellFocused={uiState.embeddedShellFocused}
|
|
placeholder={
|
|
vimEnabled
|
|
? ' ' + t("Press 'i' for INSERT mode and 'Esc' for NORMAL mode.")
|
|
: ' ' +
|
|
t(
|
|
subscriptionActive
|
|
? 'Type your request, or /usage'
|
|
: 'Type your message or @path/to/file',
|
|
)
|
|
}
|
|
promptSuggestion={uiState.promptSuggestion}
|
|
onPromptSuggestionDismiss={uiState.abortPromptSuggestion}
|
|
clipboardUnavailableShownRef={clipboardUnavailableShownRef}
|
|
voiceMicWarnedStatusRef={uiState.voiceMicWarnedStatusRef}
|
|
/>
|
|
)}
|
|
|
|
{/* Exclusive area: only one component visible at a time */}
|
|
{/* Hide footer when a confirmation dialog (e.g. ask_user_question) is active */}
|
|
{/* The footer is the only production consumer of useConfigInitMessage,
|
|
which returns a message exactly while initialization is pending — so
|
|
it has to mount on that window too, or startup shows no progress at
|
|
all. InputPrompt stays gated on isInputActive alone. */}
|
|
{(uiState.isInputActive || !uiState.isConfigInitialized) &&
|
|
!showSuggestions &&
|
|
(showShortcuts ? (
|
|
<KeyboardShortcuts />
|
|
) : (
|
|
!isScreenReaderEnabled && <Footer containerRef={footerRef} />
|
|
))}
|
|
</Box>
|
|
);
|
|
};
|