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 && (