mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* refactor(core): move codingPlan constants from cli to core package Extract Coding Plan region configs, model templates, and utility functions into packages/core/src/constants/ so both CLI and VSCode extension can import from a shared source of truth. * refactor(cli): import codingPlan constants from core instead of local path Update all CLI files to import CodingPlanRegion, CODING_PLAN_ENV_KEY, and related utilities from @qwen-code/qwen-code-core, replacing the local ../../constants/codingPlan.js imports. * feat(vscode-ide-companion): replace login flow with provider setup via VSCode Settings Replace the OAuth-based login command with a settings-driven provider configuration flow. Users now configure Coding Plan or API Key providers through VSCode Settings (qwen-code.*), which auto-syncs to ~/.qwen/settings.json. - Rename login command to auth, opening VSCode Settings panel - Add /auth2 interactive flow (QuickPick + InputBox) - Add ProviderSetupForm onboarding component with inline config - Add bidirectional sync between VSCode settings and ~/.qwen/settings.json - Add settingsWriter service for direct settings.json read/write - Add VSCode configuration schema (provider, apiKey, region, model, etc.) - Update all login/session messages to use auth terminology * refactor(vscode-ide-companion): rename auth2→auth, remove dead code, fix sync guard - Rename auth2 to auth for all message types, handlers, and slash command - Remove unused InfoBanner.tsx (128 lines, no references) - Remove dead openProviderSettings handler (no callers) - Remove redundant qwen-code.baseUrl VSCode setting (already in modelProviders) - Replace unreliable setTimeout(500) sync guard with await Promise.all + finally - Clean up old authHandler/setAuthHandler in favor of authInteractiveHandler * refactor(vscode-ide-companion): remove dead VSCode Settings plumbing, simplify sync - Remove qwen-code.modelProviders and qwen-code.model from package.json (model switching handled by chat UI's /model command, not VSCode Settings) - Remove connectWithSettings message handler and plumbing (no webview component sends this message type) - Remove handleConnectWithSettings method from WebViewProvider - Simplify syncVSCodeSettingsToQwenConfig: only sync provider/apiKey/region - Simplify syncQwenConfigToVSCodeSettings: only populate provider/apiKey/region - Simplify QwenSettingsForVSCode interface: remove modelProviders and model - Improve Onboarding UI: logo above card, better hierarchy, arrow icon on button * fix(vscode-ide-companion): add missing vscode.workspace mock in test Add onDidChangeConfiguration and getConfiguration to the vscode.workspace mock in WebViewProvider.test.ts to fix CI test failures. * fix(vscode-ide-companion): clean up stale coding plan state, add auth cancel handling, add tests - Clear CODING_PLAN_ENV_KEY and codingPlan metadata when switching to api-key mode - Add authCancelled notification when QuickPick/InputBox is dismissed - ProviderSetupForm resets button state on authCancelled - syncVSCodeSettingsToQwenConfig returns false for api-key mode (no-op) - Fix Onboarding vertical centering (flex-1 min-h-0) - Import from @qwen-code/qwen-code-core top-level instead of deep paths - Add tests: settingsWriter, ProviderSetupForm cancel, AuthMessageHandler cancel, WebViewProvider sync - Fix redundant ternary in pick() helper * fix(vscode-ide-companion): force center Onboarding against parent override Parent container uses [&>*]:items-start and [&>*]:text-left which overrides Tailwind classes. Use inline style for alignItems/justifyContent/textAlign to ensure Onboarding is always centered both horizontally and vertically. * fix(vscode-ide-companion): bundle onboarding logo * test(vscode-ide-companion): add png loader to bundle test * fix(vscode-ide-companion/webview): avoid redundant auth sync reconnects * fix(vscode-ide-companion/webview): fix auth sync typecheck * docs(vscode-ide-companion): clarify auth restoration flow * fix(webui): use bracket access for permission drawer plan content * fix(vscode-ide-companion): guard authSuccess emission on actual auth state After reconnecting in handleAuthInteractive, doInitializeAgentConnection may return without throwing even when credentials are rejected (it sends authState:false internally and returns early). Previously we unconditionally emitted authSuccess, which contradicted the failed auth state and could briefly show a success toast before re-opening the auth flow. Now we check this.authState after reconnection: only emit authSuccess when authentication actually succeeded, otherwise emit authError with a clear credentials message. Addresses review feedback from PR #3398. * fix(vscode): address auth setup review feedback * fix(vscode-ide-companion): guard concurrent auth flows, merge model providers - Add authFlowActive mutex and autoAuthTimer to WebViewProvider so startInteractiveAuth() cancels the deferred auto-auth timeout, preventing two overlapping QuickPick flows from a single command. - Change writeModelProvidersConfig() to merge new entries with existing non-target models (different envKey) instead of replacing the entire array, preserving unrelated providers like Coding Plan. * fix(vscode-ide-companion): handle apiKey clearing as de-auth signal, fix auto-auth race, clean imports - Add clearPersistedAuth() to settingsWriter.ts: removes selectedType, API keys, and coding plan metadata from ~/.qwen/settings.json - Config change handler now detects empty apiKey with active agent and triggers de-auth: clear credentials, disconnect, update authState - Auto-auth timer callback now properly sets authFlowActive mutex to prevent concurrent auth flows with startInteractiveAuth() - Add test covering the de-auth path (clearPersistedAuth + disconnect) - Fix import formatting in 7 CLI files (spacing, trailing commas) - Remove duplicate comment in attemptAuthStateRestoration() * fix(vscode-ide-companion): scope de-auth to apiKey changes only The previous de-auth logic triggered on any auth-related setting change where syncVSCodeSettingsToQwenConfig() returned false. For api-key providers this is the normal path (interactive auth owns config), so changing codingPlanRegion or provider would incorrectly wipe OPENAI_API_KEY. Now the de-auth branch only fires when e.affectsConfiguration('qwen-code.apiKey') is true AND the value is empty, preventing false-positive credential clearing. Add regression test: non-apiKey setting changes on an api-key provider must not trigger clearPersistedAuth or disconnect. * fix(vscode-ide-companion): add disconnect to mock type to fix CI typecheck The hoisted mockQwenAgentManagerInstances type was missing the disconnect property, causing TS2339 in the de-auth test assertions.
240 lines
7 KiB
JavaScript
240 lines
7 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import esbuild from 'esbuild';
|
|
import { createRequire } from 'node:module';
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { wasmLoader } from 'esbuild-plugin-wasm';
|
|
|
|
const production = process.argv.includes('--production');
|
|
const watch = process.argv.includes('--watch');
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(__dirname, '..', '..');
|
|
const rootRequire = createRequire(resolve(repoRoot, 'package.json'));
|
|
|
|
/**
|
|
* @type {import('esbuild').Plugin}
|
|
*/
|
|
const esbuildProblemMatcherPlugin = {
|
|
name: 'esbuild-problem-matcher',
|
|
|
|
setup(build) {
|
|
const isWatchMode = build.initialOptions.watch;
|
|
build.onStart(() => {
|
|
if (isWatchMode) {
|
|
console.log('[watch] build started');
|
|
}
|
|
});
|
|
build.onEnd((result) => {
|
|
result.errors.forEach(({ text, location }) => {
|
|
console.error(`✘ [ERROR] ${text}`);
|
|
console.error(
|
|
` ${location.file}:${location.line}:${location.column}:`,
|
|
);
|
|
});
|
|
if (isWatchMode) {
|
|
console.log('[watch] build finished');
|
|
}
|
|
});
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Ensure a single React copy in the webview bundle by resolving from repo root.
|
|
* Prevents mixing React 18/19 element types when nested node_modules exist.
|
|
* @type {import('esbuild').Plugin}
|
|
*/
|
|
const resolveFromRoot = (moduleId) => {
|
|
try {
|
|
return rootRequire.resolve(moduleId);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const reactDedupPlugin = {
|
|
name: 'react-dedup',
|
|
setup(build) {
|
|
const aliases = [
|
|
'react',
|
|
'react-dom',
|
|
'react-dom/client',
|
|
'react/jsx-runtime',
|
|
'react/jsx-dev-runtime',
|
|
];
|
|
|
|
for (const alias of aliases) {
|
|
build.onResolve({ filter: new RegExp(`^${alias}$`) }, () => {
|
|
const resolved = resolveFromRoot(alias);
|
|
if (!resolved) {
|
|
return undefined;
|
|
}
|
|
return { path: resolved };
|
|
});
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Resolve `*.wasm?binary` imports to embedded Uint8Array content.
|
|
* This keeps the companion bundle compatible with core's inline-WASM loader.
|
|
* @type {import('esbuild').Plugin}
|
|
*/
|
|
const wasmBinaryPlugin = {
|
|
name: 'wasm-binary',
|
|
setup(build) {
|
|
build.onResolve({ filter: /\.wasm\?binary$/ }, (args) => {
|
|
const specifier = args.path.replace(/\?binary$/, '');
|
|
const localRequire = createRequire(
|
|
resolve(args.resolveDir || repoRoot, '_dummy_.js'),
|
|
);
|
|
return {
|
|
path: localRequire.resolve(specifier),
|
|
namespace: 'wasm-binary',
|
|
};
|
|
});
|
|
|
|
build.onLoad({ filter: /.*/, namespace: 'wasm-binary' }, (args) => ({
|
|
contents: readFileSync(args.path),
|
|
loader: 'binary',
|
|
}));
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @type {import('esbuild').Plugin}
|
|
*/
|
|
const cssInjectPlugin = {
|
|
name: 'css-inject',
|
|
setup(build) {
|
|
// Handle CSS files
|
|
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
|
const fs = await import('fs');
|
|
const postcss = (await import('postcss')).default;
|
|
const tailwindcss = (await import('tailwindcss')).default;
|
|
const autoprefixer = (await import('autoprefixer')).default;
|
|
|
|
let css = await fs.promises.readFile(args.path, 'utf8');
|
|
|
|
// For styles.css, we need to resolve @import statements
|
|
if (args.path.endsWith('styles.css')) {
|
|
// Read all imported CSS files and inline them
|
|
const importRegex = /@import\s+'([^']+)';/g;
|
|
let match;
|
|
const basePath = args.path.substring(0, args.path.lastIndexOf('/'));
|
|
while ((match = importRegex.exec(css)) !== null) {
|
|
const importPath = match[1];
|
|
// Resolve relative paths correctly
|
|
let fullPath;
|
|
if (importPath.startsWith('./')) {
|
|
fullPath = basePath + importPath.substring(1);
|
|
} else if (importPath.startsWith('../')) {
|
|
fullPath = basePath + '/' + importPath;
|
|
} else {
|
|
fullPath = basePath + '/' + importPath;
|
|
}
|
|
|
|
try {
|
|
const importedCss = await fs.promises.readFile(fullPath, 'utf8');
|
|
css = css.replace(match[0], importedCss);
|
|
} catch (err) {
|
|
console.warn(`Could not import ${fullPath}: ${err.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process with PostCSS (Tailwind + Autoprefixer)
|
|
const result = await postcss([tailwindcss, autoprefixer]).process(css, {
|
|
from: args.path,
|
|
to: args.path,
|
|
});
|
|
|
|
return {
|
|
contents: `
|
|
const style = document.createElement('style');
|
|
style.textContent = ${JSON.stringify(result.css)};
|
|
document.head.appendChild(style);
|
|
`,
|
|
loader: 'js',
|
|
};
|
|
});
|
|
},
|
|
};
|
|
|
|
async function main() {
|
|
// Build extension
|
|
const extensionCtx = await esbuild.context({
|
|
entryPoints: ['src/extension.ts'],
|
|
bundle: true,
|
|
format: 'cjs',
|
|
minify: production,
|
|
sourcemap: !production,
|
|
sourcesContent: false,
|
|
platform: 'node',
|
|
outfile: 'dist/extension.cjs',
|
|
external: ['vscode'],
|
|
logLevel: 'silent',
|
|
banner: {
|
|
js: `const import_meta = { url: require('url').pathToFileURL(__filename).href };`,
|
|
},
|
|
define: {
|
|
'import.meta.url': 'import_meta.url',
|
|
},
|
|
plugins: [
|
|
wasmBinaryPlugin,
|
|
wasmLoader({ mode: 'embedded' }),
|
|
/* add to the end of plugins array */
|
|
esbuildProblemMatcherPlugin,
|
|
],
|
|
loader: { '.node': 'file' },
|
|
});
|
|
|
|
// Build webview
|
|
const webviewCtx = await esbuild.context({
|
|
entryPoints: ['src/webview/index.tsx'],
|
|
bundle: true,
|
|
format: 'iife',
|
|
minify: production,
|
|
sourcemap: !production,
|
|
sourcesContent: false,
|
|
platform: 'browser',
|
|
outfile: 'dist/webview.js',
|
|
// @qwen-code/qwen-code-core is a peer dependency of @qwen-code/webui.
|
|
// Since @qwen-code/webui marks it as external in its own Vite build, the
|
|
// browser bundle must also mark it external to avoid bundling Node.js-only
|
|
// modules (undici, @grpc/grpc-js, fs, stream, etc.) into the webview.
|
|
// The wildcard ensures deep sub-path imports (e.g.
|
|
// '@qwen-code/qwen-code-core/src/core/tokenLimits.js') are also excluded;
|
|
// without it esbuild only matches the bare package name and attempts to
|
|
// bundle the sub-path, which triggers "Dynamic require is not supported"
|
|
// at runtime in the browser.
|
|
external: ['@qwen-code/qwen-code-core', '@qwen-code/qwen-code-core/*'],
|
|
logLevel: 'silent',
|
|
plugins: [reactDedupPlugin, cssInjectPlugin, esbuildProblemMatcherPlugin],
|
|
jsx: 'automatic', // Use new JSX transform (React 17+)
|
|
loader: {
|
|
'.png': 'dataurl',
|
|
},
|
|
define: {
|
|
'process.env.NODE_ENV': production ? '"production"' : '"development"',
|
|
},
|
|
});
|
|
|
|
if (watch) {
|
|
await Promise.all([extensionCtx.watch(), webviewCtx.watch()]);
|
|
} else {
|
|
await Promise.all([extensionCtx.rebuild(), webviewCtx.rebuild()]);
|
|
await Promise.all([extensionCtx.dispose(), webviewCtx.dispose()]);
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|