mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
Merge branch 'main' into mingholy/fix/api-key-auth
This commit is contained in:
commit
54c899ce82
4 changed files with 52 additions and 6 deletions
|
|
@ -56,6 +56,7 @@ vi.mock('./StreamingStatus', () => ({
|
|||
<div
|
||||
data-testid="pane-streaming"
|
||||
data-started-at={props.startedAt === undefined ? 'none' : String(props.startedAt)}
|
||||
data-show-phrase={String(props.showPhrase)}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
|
@ -163,6 +164,13 @@ describe('ChatPane', () => {
|
|||
expect(container!.textContent).toContain('Refactor core');
|
||||
});
|
||||
|
||||
it('suppresses the rotating loading phrase in its compact status', () => {
|
||||
render();
|
||||
expect(testid('pane-streaming')?.getAttribute('data-show-phrase')).toBe(
|
||||
'false',
|
||||
);
|
||||
});
|
||||
|
||||
it('sends a prompt to its own session on submit', () => {
|
||||
render();
|
||||
act(() =>
|
||||
|
|
|
|||
|
|
@ -206,7 +206,9 @@ export function ChatPane({ title, isCurrent, onClose, onError }: ChatPaneProps)
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<StreamingStatus startedAt={activeTurnStartedAt} />
|
||||
{/* Panes keep the composer status compact: spinner + elapsed time +
|
||||
token count + cancel hint, but no rotating "witty" loading phrase. */}
|
||||
<StreamingStatus startedAt={activeTurnStartedAt} showPhrase={false} />
|
||||
<ChatEditor
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ afterEach(() => {
|
|||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function render(customization: WebShellCustomization = {}): HTMLElement {
|
||||
function render(
|
||||
customization: WebShellCustomization = {},
|
||||
props: { showPhrase?: boolean } = {},
|
||||
): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
|
@ -43,7 +46,7 @@ function render(customization: WebShellCustomization = {}): HTMLElement {
|
|||
root.render(
|
||||
<I18nProvider language="en">
|
||||
<WebShellCustomizationProvider value={customization}>
|
||||
<StreamingStatus />
|
||||
<StreamingStatus {...props} />
|
||||
</WebShellCustomizationProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
|
@ -90,6 +93,23 @@ describe('StreamingStatus loading phrases', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('hides the phrase but keeps the dynamic status when showPhrase is false', () => {
|
||||
pinPhraseSelection();
|
||||
// A resolver that would otherwise supply a phrase must still be suppressed.
|
||||
const container = render({ loadingPhrases: () => ['should not appear'] }, {
|
||||
showPhrase: false,
|
||||
});
|
||||
// The witty phrase (the "废话文学") is gone: no label span.
|
||||
expect(labelText(container)).toBeUndefined();
|
||||
expect(container.textContent).not.toContain('should not appear');
|
||||
// But the dynamic status stays: spinner + meta (elapsed time + cancel hint).
|
||||
const spans = container.firstElementChild?.querySelectorAll('span') ?? [];
|
||||
expect(spans.length).toBe(2);
|
||||
expect(spans[0]?.textContent).not.toBe(''); // spinner frame
|
||||
expect(container.textContent).toContain('esc to cancel'); // meta/cancel hint
|
||||
expect(container.textContent).toMatch(/\ds/); // elapsed time, e.g. "0s"
|
||||
});
|
||||
|
||||
it('falls back to the built-in defaults when the resolver returns undefined', () => {
|
||||
pinPhraseSelection();
|
||||
const container = render({ loadingPhrases: () => undefined });
|
||||
|
|
|
|||
|
|
@ -12,11 +12,21 @@ import styles from './StreamingStatus.module.css';
|
|||
|
||||
interface StreamingStatusProps {
|
||||
startedAt?: number;
|
||||
/**
|
||||
* When false, hide the rotating "witty" loading phrase and skip its rotation
|
||||
* timer entirely — the spinner, elapsed time, token count, and cancel hint
|
||||
* still render. Split-view panes pass false to keep each pane's composer
|
||||
* status compact. Defaults to true (the main chat shows the phrase).
|
||||
*/
|
||||
showPhrase?: boolean;
|
||||
}
|
||||
|
||||
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
|
||||
export function StreamingStatus({ startedAt }: StreamingStatusProps) {
|
||||
export function StreamingStatus({
|
||||
startedAt,
|
||||
showPhrase = true,
|
||||
}: StreamingStatusProps) {
|
||||
const streamingState = useStreamingState();
|
||||
const { estimatedOutputTokens, isReceivingContent } =
|
||||
useStreamingLoadingMetrics();
|
||||
|
|
@ -71,6 +81,10 @@ export function StreamingStatus({ startedAt }: StreamingStatusProps) {
|
|||
}, [isActive, startedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
// Callers that hide the phrase (e.g. split-view panes) don't need the
|
||||
// rotation timer at all — bail before arming it so panes don't each run a
|
||||
// needless interval and re-render on every tick.
|
||||
if (!showPhrase) return;
|
||||
if (streamingState === 'idle') {
|
||||
setLoadingPhrase(resolvePhrases(language)[0] ?? '');
|
||||
return;
|
||||
|
|
@ -90,7 +104,7 @@ export function StreamingStatus({ startedAt }: StreamingStatusProps) {
|
|||
pickPhrase();
|
||||
const interval = setInterval(pickPhrase, PHRASE_CHANGE_INTERVAL_MS);
|
||||
return () => clearInterval(interval);
|
||||
}, [language, streamingState, resolvePhrases]);
|
||||
}, [language, streamingState, resolvePhrases, showPhrase]);
|
||||
|
||||
useEffect(() => {
|
||||
if (streamingState === 'idle') return;
|
||||
|
|
@ -113,7 +127,9 @@ export function StreamingStatus({ startedAt }: StreamingStatusProps) {
|
|||
return (
|
||||
<div className={styles.status}>
|
||||
<span className={styles.spinner}>{spinnerChar}</span>
|
||||
{loadingPhrase && <span className={styles.label}>{loadingPhrase}</span>}
|
||||
{showPhrase && loadingPhrase && (
|
||||
<span className={styles.label}>{loadingPhrase}</span>
|
||||
)}
|
||||
<span className={styles.meta}>
|
||||
({timeStr}
|
||||
{tokenStr} · {t('stream.cancel')})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue