mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
feat(web-shell): show the qwen-code version in the sidebar footer (#6222)
* feat(web-shell): show the qwen-code version in the sidebar footer The Web Shell had no visible version. Show the running qwen-code version (from the daemon capabilities) in the sidebar footer, inline with the Settings button so it stays visible without taking its own row. Render the version consistently wherever it appears: - Prefix "v" only for a real semver release; a non-semver fallback such as "unknown" is shown as-is, so we never render a bogus "vunknown". Applied to the Web Shell badge and the TUI header. - Dev builds (scripts/dev.js) now report the real package version instead of the "dev" sentinel, matching scripts/start.js, so the UI shows the actual version (e.g. v0.19.4). DEV=true / NODE_ENV=development remain the signals that mark a dev build. * test: add readFileSync to node:fs mock in dev.test.js scripts/dev.js now reads package.json via readFileSync at module load to report the real CLI_VERSION, but the node:fs mock in dev.test.js did not export readFileSync, causing vitest to throw "No readFileSync export is defined on the node:fs mock" and failing the suite.
This commit is contained in:
parent
23c6c7032a
commit
da22360c25
7 changed files with 152 additions and 2 deletions
|
|
@ -52,6 +52,15 @@ describe('<Header />', () => {
|
|||
expect(lastFrame()).toContain('v1.0.0');
|
||||
});
|
||||
|
||||
it('shows a non-semver fallback like "unknown" without a bogus "v" prefix', () => {
|
||||
const { lastFrame } = render(
|
||||
<Header {...defaultProps} version="unknown" />,
|
||||
);
|
||||
const frame = lastFrame() ?? '';
|
||||
expect(frame).toContain('(unknown)');
|
||||
expect(frame).not.toContain('vunknown');
|
||||
});
|
||||
|
||||
it('displays auth type and model', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('Qwen OAuth');
|
||||
|
|
|
|||
|
|
@ -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<HeaderProps> = ({
|
|||
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<HeaderProps> = ({
|
|||
<Text bold color={theme.text.accent}>
|
||||
{customBannerTitle ? customBannerTitle : '>_ Qwen Code'}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}> (v{version})</Text>
|
||||
<Text color={theme.text.secondary}> ({versionLabel})</Text>
|
||||
</Text>
|
||||
{/* Subtitle (when set) replaces the blank spacer row. We always
|
||||
emit a row here so the auth/model line stays at the same
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<I18nProvider language="en">
|
||||
<WebShellSidebar
|
||||
collapsed={collapsed}
|
||||
onCollapsedChange={noop}
|
||||
onOpenSettings={noop}
|
||||
onNewSession={() => false}
|
||||
onLoadSession={noop}
|
||||
onError={noop}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
});
|
||||
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/);
|
||||
});
|
||||
});
|
||||
|
|
@ -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({
|
|||
</span>
|
||||
{!collapsed && <span>{t('sidebar.settings')}</span>}
|
||||
</button>
|
||||
{!collapsed && versionLabel && (
|
||||
<span className={styles.version} title={`Qwen Code ${versionLabel}`}>
|
||||
{versionLabel}
|
||||
</span>
|
||||
)}
|
||||
{!mobileOpen && (
|
||||
<button
|
||||
className={styles.collapseButton}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,14 @@ import {
|
|||
existsSync,
|
||||
symlinkSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir, platform } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(__dirname, '..');
|
||||
const cliPackageDir = join(root, 'packages', 'cli');
|
||||
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
|
||||
|
||||
// Ensure qc-helper bundled skill can find user docs in dev mode.
|
||||
// In dev, import.meta.url resolves to the source tree, so the bundled skill
|
||||
|
|
@ -102,7 +104,10 @@ const importFlag = `--import ${pathToFileURL(registerPath).href}`;
|
|||
const env = {
|
||||
...process.env,
|
||||
DEV: 'true',
|
||||
CLI_VERSION: 'dev',
|
||||
// Report the real package version (like scripts/start.js) so the UI shows
|
||||
// e.g. "v0.19.4" instead of "dev". DEV=true / NODE_ENV=development remain the
|
||||
// signals that distinguish a dev build.
|
||||
CLI_VERSION: pkg.version,
|
||||
NODE_ENV: 'development',
|
||||
NODE_OPTIONS: `${existingNodeOptions} --expose-gc ${importFlag}`.trim(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ vi.mock('node:fs', () => ({
|
|||
existsSync: existsSyncMock,
|
||||
symlinkSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
readFileSync: vi.fn(() => JSON.stringify({ version: '0.0.0-test' })),
|
||||
}));
|
||||
|
||||
const normalizePath = (path) => String(path).replaceAll('\\', '/');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue