diff --git a/packages/cli/src/ui/components/Header.test.tsx b/packages/cli/src/ui/components/Header.test.tsx index 833774d1ff..d907cbb7de 100644 --- a/packages/cli/src/ui/components/Header.test.tsx +++ b/packages/cli/src/ui/components/Header.test.tsx @@ -52,6 +52,15 @@ describe('
', () => { expect(lastFrame()).toContain('v1.0.0'); }); + it('shows a non-semver fallback like "unknown" without a bogus "v" prefix', () => { + const { lastFrame } = render( +
, + ); + const frame = lastFrame() ?? ''; + expect(frame).toContain('(unknown)'); + expect(frame).not.toContain('vunknown'); + }); + it('displays auth type and model', () => { const { lastFrame } = render(
); expect(lastFrame()).toContain('Qwen OAuth'); diff --git a/packages/cli/src/ui/components/Header.tsx b/packages/cli/src/ui/components/Header.tsx index 94ec219377..e59d49f882 100644 --- a/packages/cli/src/ui/components/Header.tsx +++ b/packages/cli/src/ui/components/Header.tsx @@ -49,6 +49,16 @@ function formatAuthDisplayType( } } +/** + * Format the version for display. Real semver releases get a "v" prefix + * ("v0.19.4"); a non-semver fallback such as "unknown" (from getCliVersion when + * the package version can't be resolved) is shown as-is so we never render a + * bogus "vunknown". + */ +function formatVersionLabel(version: string): string { + return /^\d/.test(version) ? `v${version}` : version; +} + interface HeaderProps { /** * Width-aware override for the logo column. Each tier is a sanitized @@ -88,6 +98,7 @@ export const Header: React.FC = ({ const { columns: terminalWidth } = useTerminalSize(); const formattedAuthType = formatAuthDisplayType(authDisplayType); + const versionLabel = formatVersionLabel(version); // Calculate available space properly: // First determine if logo can be shown, then use remaining space for path @@ -209,7 +220,7 @@ export const Header: React.FC = ({ {customBannerTitle ? customBannerTitle : '>_ Qwen Code'} - (v{version}) + ({versionLabel}) {/* Subtitle (when set) replaces the blank spacer row. We always emit a row here so the auth/model line stays at the same diff --git a/packages/web-shell/client/components/sidebar/WebShellSidebar.module.css b/packages/web-shell/client/components/sidebar/WebShellSidebar.module.css index 0c5e13ca44..3e81ef6b78 100644 --- a/packages/web-shell/client/components/sidebar/WebShellSidebar.module.css +++ b/packages/web-shell/client/components/sidebar/WebShellSidebar.module.css @@ -578,6 +578,16 @@ flex: 0 0 auto; } +.version { + flex: 0 0 auto; + color: var(--muted-foreground); + font-size: 12px; + line-height: 1; + letter-spacing: 0.02em; + white-space: nowrap; + user-select: text; +} + .collapsed .newChatButton, .collapsed .footerButton, .collapsed .projectRow { diff --git a/packages/web-shell/client/components/sidebar/WebShellSidebar.test.tsx b/packages/web-shell/client/components/sidebar/WebShellSidebar.test.tsx new file mode 100644 index 0000000000..cb88fa13aa --- /dev/null +++ b/packages/web-shell/client/components/sidebar/WebShellSidebar.test.tsx @@ -0,0 +1,101 @@ +// @vitest-environment jsdom +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; + +const { mockConnection } = vi.hoisted(() => ({ + mockConnection: { + status: 'connected', + sessionId: null as string | null, + workspaceCwd: '/tmp/project', + capabilities: { qwenCodeVersion: '1.2.3' } as + | { qwenCodeVersion?: string } + | undefined, + }, +})); + +vi.mock('@qwen-code/webui/daemon-react-sdk', () => ({ + useConnection: () => mockConnection, + useActions: () => ({ renameSession: vi.fn() }), + useSessions: () => ({ + sessions: [], + loading: false, + error: null, + reload: vi.fn(), + deleteSession: vi.fn(), + }), +})); + +const { I18nProvider } = await import('../../i18n'); +const { WebShellSidebar } = await import('./WebShellSidebar'); + +( + globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } +).IS_REACT_ACT_ENVIRONMENT = true; + +const mounted: Array<{ root: Root; container: HTMLElement }> = []; + +const noop = () => {}; + +function renderSidebar(collapsed: boolean): HTMLElement { + const container = document.createElement('div'); + document.body.appendChild(container); + const root = createRoot(container); + act(() => { + root.render( + + false} + onLoadSession={noop} + onError={noop} + /> + , + ); + }); + mounted.push({ root, container }); + return container; +} + +beforeEach(() => { + mockConnection.capabilities = { qwenCodeVersion: '1.2.3' }; +}); + +afterEach(() => { + for (const { root, container } of mounted.splice(0)) { + act(() => root.unmount()); + container.remove(); + } +}); + +describe('WebShellSidebar — version footer', () => { + it('shows the qwen-code version in the footer when expanded', () => { + const container = renderSidebar(false); + const badge = container.querySelector('[title="Qwen Code v1.2.3"]'); + expect(badge).not.toBeNull(); + expect(badge?.textContent).toBe('v1.2.3'); + }); + + it('renders a non-semver fallback (e.g. "unknown") without a bogus "v" prefix', () => { + mockConnection.capabilities = { qwenCodeVersion: 'unknown' }; + const container = renderSidebar(false); + const badge = container.querySelector('[title="Qwen Code unknown"]'); + expect(badge).not.toBeNull(); + expect(badge?.textContent).toBe('unknown'); + expect(container.textContent ?? '').not.toContain('vunknown'); + }); + + it('hides the version when the sidebar is collapsed', () => { + const container = renderSidebar(true); + expect(container.querySelector('[title="Qwen Code v1.2.3"]')).toBeNull(); + expect(container.textContent ?? '').not.toContain('v1.2.3'); + }); + + it('renders no version badge when the daemon reports none', () => { + mockConnection.capabilities = undefined; + const container = renderSidebar(false); + expect(container.textContent ?? '').not.toMatch(/v\d/); + }); +}); diff --git a/packages/web-shell/client/components/sidebar/WebShellSidebar.tsx b/packages/web-shell/client/components/sidebar/WebShellSidebar.tsx index 5b4f10af00..bc66117aab 100644 --- a/packages/web-shell/client/components/sidebar/WebShellSidebar.tsx +++ b/packages/web-shell/client/components/sidebar/WebShellSidebar.tsx @@ -214,6 +214,14 @@ export function WebShellSidebar({ const currentSessionId = connection.sessionId; const projectName = getWorkspaceName(connection.workspaceCwd) || t('sidebar.projectFallback'); + const qwenCodeVersion = connection.capabilities?.qwenCodeVersion || ''; + // Numeric releases render as "v1.2.3"; a non-semver fallback such as + // "unknown" is shown as-is so we never produce a bogus "vunknown". + const versionLabel = qwenCodeVersion + ? /^\d/.test(qwenCodeVersion) + ? `v${qwenCodeVersion}` + : qwenCodeVersion + : ''; const sidebarStyle = { '--web-shell-sidebar-width': `${sidebarWidth}px`, } as CSSProperties; @@ -940,6 +948,11 @@ export function WebShellSidebar({ {!collapsed && {t('sidebar.settings')}} + {!collapsed && versionLabel && ( + + {versionLabel} + + )} {!mobileOpen && (