diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5d40f391a9..a9df0e52c9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -365,6 +365,64 @@ jobs:
name: 'coverage-reports-22.x-ubuntu-latest'
path: 'packages/*/coverage'
+ web_shell_e2e_smoke:
+ name: 'web-shell E2E Smoke (ubuntu-latest, Node 22.x)'
+ needs:
+ - 'classify_pr'
+ - 'test'
+ if: |-
+ ${{
+ !cancelled() &&
+ github.event_name == 'pull_request' &&
+ needs.classify_pr.outputs.skip_ci != 'true' &&
+ needs.test.outputs.ci_profile == 'full'
+ }}
+ runs-on: 'ubuntu-latest'
+ timeout-minutes: 20
+ permissions:
+ contents: 'read'
+ steps:
+ - name: 'Checkout'
+ uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
+ with:
+ ref: "${{ format('refs/pull/{0}/head', github.event.pull_request.number) }}"
+ fetch-depth: 1
+
+ - name: 'Set up Node.js 22.x'
+ uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
+ with:
+ node-version: '22.x'
+ cache: 'npm'
+ cache-dependency-path: 'package-lock.json'
+ registry-url: 'https://registry.npmjs.org/'
+
+ - name: 'Configure npm for rate limiting'
+ run: |-
+ npm config set fetch-retry-mintimeout 20000
+ npm config set fetch-retry-maxtimeout 120000
+ npm config set fetch-retries 5
+ npm config set fetch-timeout 300000
+
+ - name: 'Install dependencies'
+ run: |-
+ npm ci --prefer-offline --no-audit --progress=false
+
+ - name: 'Install Playwright Chromium'
+ run: 'npx playwright install --with-deps chromium'
+
+ - name: 'Run web-shell browser smoke'
+ run: 'npm run test:e2e:smoke --workspace=packages/web-shell'
+
+ - name: 'Upload web-shell Playwright artifacts'
+ if: '${{ always() }}'
+ uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
+ with:
+ name: 'web-shell-e2e-smoke'
+ path: |-
+ packages/web-shell/client/e2e/test-results
+ packages/web-shell/client/e2e/playwright-report
+ if-no-files-found: 'ignore'
+
# macOS/Windows: slowest/costliest runners, rare platform regressions — run
# only in the merge queue. Skipped on PR (ubuntu is the fast PR signal) and on
# push (the queue already tested the merged tree, so a post-merge re-run is
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index c5afbf1d38..8e43737886 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -144,3 +144,47 @@ jobs:
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
run: 'npm run test:e2e'
+
+ web-shell-browser-regression:
+ name: 'web-shell Browser Regression'
+ runs-on: 'ubuntu-latest'
+ if: |-
+ ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
+ steps:
+ - name: 'Checkout'
+ uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
+
+ - name: 'Set up Node.js'
+ uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+ cache-dependency-path: 'package-lock.json'
+ registry-url: 'https://registry.npmjs.org/'
+
+ - name: 'Configure npm for rate limiting'
+ run: |-
+ npm config set fetch-retry-mintimeout 20000
+ npm config set fetch-retry-maxtimeout 120000
+ npm config set fetch-retries 5
+ npm config set fetch-timeout 300000
+
+ - name: 'Install dependencies'
+ run: |-
+ npm ci --prefer-offline --no-audit --progress=false
+
+ - name: 'Install Playwright Chromium'
+ run: 'npx playwright install --with-deps chromium'
+
+ - name: 'Run web-shell browser regression'
+ run: 'npm run test:e2e --workspace=packages/web-shell'
+
+ - name: 'Upload web-shell Playwright artifacts'
+ if: '${{ always() }}'
+ uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
+ with:
+ name: 'web-shell-browser-regression'
+ path: |-
+ packages/web-shell/client/e2e/test-results
+ packages/web-shell/client/e2e/playwright-report
+ if-no-files-found: 'ignore'
diff --git a/.gitignore b/.gitignore
index e7e956c2c7..cb894b6007 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,6 +76,8 @@ bundle
# Test report files
junit.xml
packages/*/coverage/
+packages/web-shell/client/e2e/playwright-report/
+packages/web-shell/client/e2e/test-results/
# PR body draft
pr_body.md
diff --git a/eslint.config.js b/eslint.config.js
index f74416e232..08dca2c70f 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -170,6 +170,82 @@ export default tseslint.config(
'default-case': 'error',
},
},
+ {
+ files: [
+ 'packages/web-shell/client/**/*.{ts,tsx}',
+ 'packages/web-shell/*.config.ts',
+ ],
+ plugins: {
+ import: importPlugin,
+ },
+ settings: {
+ 'import/resolver': {
+ node: true,
+ },
+ },
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ ...globals.es2021,
+ ...globals.node,
+ },
+ },
+ rules: {
+ 'react/prop-types': 'off',
+ '@typescript-eslint/consistent-type-imports': [
+ 'error',
+ { disallowTypeAnnotations: false },
+ ],
+ '@typescript-eslint/no-explicit-any': 'error',
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ caughtErrorsIgnorePattern: '^_',
+ },
+ ],
+ 'no-console': ['error', { allow: ['warn', 'error'] }],
+ 'no-debugger': 'error',
+ 'object-shorthand': 'error',
+ 'prefer-const': ['error', { destructuring: 'all' }],
+ },
+ },
+ {
+ files: [
+ 'packages/web-shell/client/**/*.test.{ts,tsx}',
+ 'packages/web-shell/client/test/**/*.{ts,tsx}',
+ ],
+ plugins: {
+ vitest,
+ },
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ ...globals.es2021,
+ ...globals.vitest,
+ },
+ },
+ rules: {
+ ...vitest.configs.recommended.rules,
+ 'vitest/expect-expect': 'off',
+ 'vitest/no-commented-out-tests': 'off',
+ 'no-console': 'off',
+ },
+ },
+ {
+ files: ['packages/web-shell/client/e2e/**/*.{ts,tsx}'],
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ ...globals.es2021,
+ ...globals.node,
+ },
+ },
+ rules: {
+ 'no-console': 'off',
+ },
+ },
{
// Enforce kebab-case filenames
files: ['packages/core/src/**/*.ts', 'packages/cli/src/**/*.ts'],
diff --git a/package-lock.json b/package-lock.json
index 41267ce4ed..a02e6d82f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27380,6 +27380,7 @@
"shiki": "^1.29.2"
},
"devDependencies": {
+ "@playwright/test": "^1.57.0",
"@qwen-code/sdk": "file:../sdk-typescript",
"@qwen-code/webui": "file:../webui",
"@types/node": "^22.0.0",
diff --git a/packages/web-shell/client/App.tsx b/packages/web-shell/client/App.tsx
index 6cf2b93481..d8592e0e4b 100644
--- a/packages/web-shell/client/App.tsx
+++ b/packages/web-shell/client/App.tsx
@@ -627,12 +627,6 @@ function isAlreadyDispatched(error: unknown): error is AlreadyDispatchedError {
);
}
-function logSessionNoticesHook(notices: readonly DaemonSessionNotice[]): void {
- if (notices.length > 0) {
- console.info('[web-shell] useSessionNotices()', { notices });
- }
-}
-
function shouldToastNotice(notice: DaemonSessionNotice): boolean {
return (
notice.category === 'validation' ||
@@ -1817,11 +1811,9 @@ export function App({
(error: unknown, fallback: string) => {
if (isAbortError(error)) return;
if (isDaemonTurnError(error)) {
- console.debug('[web-shell] turn error rendered in transcript', error);
return;
}
if (isAlreadyDispatched(error)) {
- console.debug('[web-shell] error already handled by notice', error);
return;
}
const message = formatError(error, fallback);
@@ -1913,13 +1905,10 @@ export function App({
);
useEffect(() => {
- logSessionNoticesHook(notices);
for (const notice of notices) {
if (shouldToastNotice(notice)) {
pushToast(toastToneFromNotice(notice), notice.message);
- } else if (notice.category === 'lifecycle') {
- console.debug('[web-shell] daemon notice', notice);
- } else {
+ } else if (notice.category !== 'lifecycle') {
console.warn('[web-shell] daemon notice', notice);
}
dismissNotice(notice.id);
diff --git a/packages/web-shell/client/components/ChatEditor.tsx b/packages/web-shell/client/components/ChatEditor.tsx
index 5761b8d485..81315a1e50 100644
--- a/packages/web-shell/client/components/ChatEditor.tsx
+++ b/packages/web-shell/client/components/ChatEditor.tsx
@@ -704,7 +704,12 @@ function SlashCommandPanel({
} as CSSProperties;
return createPortal(
-
+
+
{
setModeDropdownOpen(false);
@@ -1457,7 +1463,7 @@ export const ChatEditor = memo(
!
)}
-
+
@@ -1486,6 +1492,7 @@ export const ChatEditor = memo(