diff --git a/.agents/skills/component-refactoring/SKILL.md b/.agents/skills/component-refactoring/SKILL.md
index 7006c382c8..140e0ef434 100644
--- a/.agents/skills/component-refactoring/SKILL.md
+++ b/.agents/skills/component-refactoring/SKILL.md
@@ -480,4 +480,4 @@ const useButtonState = () => {
### Related Skills
- `frontend-testing` - For testing refactored components
-- `web/testing/testing.md` - Testing specification
+- `web/docs/test.md` - Testing specification
diff --git a/.agents/skills/frontend-testing/SKILL.md b/.agents/skills/frontend-testing/SKILL.md
index 0716c81ef7..280fcb6341 100644
--- a/.agents/skills/frontend-testing/SKILL.md
+++ b/.agents/skills/frontend-testing/SKILL.md
@@ -7,7 +7,7 @@ description: Generate Vitest + React Testing Library tests for Dify frontend com
This skill enables Claude to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.
-> **⚠️ Authoritative Source**: This skill is derived from `web/testing/testing.md`. Use Vitest mock/timer APIs (`vi.*`).
+> **⚠️ Authoritative Source**: This skill is derived from `web/docs/test.md`. Use Vitest mock/timer APIs (`vi.*`).
## When to Apply This Skill
@@ -309,7 +309,7 @@ For more detailed information, refer to:
### Primary Specification (MUST follow)
-- **`web/testing/testing.md`** - The canonical testing specification. This skill is derived from this document.
+- **`web/docs/test.md`** - The canonical testing specification. This skill is derived from this document.
### Reference Examples in Codebase
diff --git a/.agents/skills/frontend-testing/references/workflow.md b/.agents/skills/frontend-testing/references/workflow.md
index 009c3e013b..bc4ed8285a 100644
--- a/.agents/skills/frontend-testing/references/workflow.md
+++ b/.agents/skills/frontend-testing/references/workflow.md
@@ -4,7 +4,7 @@ This guide defines the workflow for generating tests, especially for complex com
## Scope Clarification
-This guide addresses **multi-file workflow** (how to process multiple test files). For coverage requirements within a single test file, see `web/testing/testing.md` § Coverage Goals.
+This guide addresses **multi-file workflow** (how to process multiple test files). For coverage requirements within a single test file, see `web/docs/test.md` § Coverage Goals.
| Scope | Rule |
|-------|------|
diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml
index 190e00d9fe..52e3272f99 100644
--- a/.github/workflows/api-tests.yml
+++ b/.github/workflows/api-tests.yml
@@ -72,6 +72,7 @@ jobs:
OPENDAL_FS_ROOT: /tmp/dify-storage
run: |
uv run --project api pytest \
+ -n auto \
--timeout "${PYTEST_TIMEOUT:-180}" \
api/tests/integration_tests/workflow \
api/tests/integration_tests/tools \
diff --git a/AGENTS.md b/AGENTS.md
index 7d96ac3a6d..51fa6e4527 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -7,7 +7,7 @@ Dify is an open-source platform for developing LLM applications with an intuitiv
The codebase is split into:
- **Backend API** (`/api`): Python Flask application organized with Domain-Driven Design
-- **Frontend Web** (`/web`): Next.js 15 application using TypeScript and React 19
+- **Frontend Web** (`/web`): Next.js application using TypeScript and React
- **Docker deployment** (`/docker`): Containerized deployment configurations
## Backend Workflow
@@ -18,36 +18,7 @@ The codebase is split into:
## Frontend Workflow
-```bash
-cd web
-pnpm lint:fix
-pnpm type-check:tsgo
-pnpm test
-```
-
-### Frontend Linting
-
-ESLint is used for frontend code quality. Available commands:
-
-```bash
-# Lint all files (report only)
-pnpm lint
-
-# Lint and auto-fix issues
-pnpm lint:fix
-
-# Lint specific files or directories
-pnpm lint:fix app/components/base/button/
-pnpm lint:fix app/components/base/button/index.tsx
-
-# Lint quietly (errors only, no warnings)
-pnpm lint:quiet
-
-# Check code complexity
-pnpm lint:complexity
-```
-
-**Important**: Always run `pnpm lint:fix` before committing. The pre-commit hook runs `lint-staged` which only lints staged files.
+- Read `web/AGENTS.md` for details
## Testing & Quality Practices
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 20a7d6c6f6..d7f007af67 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -77,7 +77,7 @@ How we prioritize:
For setting up the frontend service, please refer to our comprehensive [guide](https://github.com/langgenius/dify/blob/main/web/README.md) in the `web/README.md` file. This document provides detailed instructions to help you set up the frontend environment properly.
-**Testing**: All React components must have comprehensive test coverage. See [web/testing/testing.md](https://github.com/langgenius/dify/blob/main/web/testing/testing.md) for the canonical frontend testing guidelines and follow every requirement described there.
+**Testing**: All React components must have comprehensive test coverage. See [web/docs/test.md](https://github.com/langgenius/dify/blob/main/web/docs/test.md) for the canonical frontend testing guidelines and follow every requirement described there.
#### Backend
diff --git a/Makefile b/Makefile
index 20cede9a5e..984e8676ee 100644
--- a/Makefile
+++ b/Makefile
@@ -80,7 +80,7 @@ test:
echo "Target: $(TARGET_TESTS)"; \
uv run --project api --dev pytest $(TARGET_TESTS); \
else \
- uv run --project api --dev dev/pytest/pytest_unit_tests.sh; \
+ PYTEST_XDIST_ARGS="-n auto" uv run --project api --dev dev/pytest/pytest_unit_tests.sh; \
fi
@echo "✅ Tests complete"
diff --git a/api/pyproject.toml b/api/pyproject.toml
index 575c1434c5..af2dba6fac 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -175,6 +175,7 @@ dev = [
# "locust>=2.40.4", # Temporarily removed due to compatibility issues. Uncomment when resolved.
"sseclient-py>=1.8.0",
"pytest-timeout>=2.4.0",
+ "pytest-xdist>=3.8.0",
]
############################################################
diff --git a/api/tests/unit_tests/conftest.py b/api/tests/unit_tests/conftest.py
index c5e1576186..e3c1a617f7 100644
--- a/api/tests/unit_tests/conftest.py
+++ b/api/tests/unit_tests/conftest.py
@@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
+from sqlalchemy import create_engine
# Getting the absolute path of the current file's directory
ABS_PATH = os.path.dirname(os.path.abspath(__file__))
@@ -36,6 +37,7 @@ import sys
sys.path.insert(0, PROJECT_DIR)
+from core.db.session_factory import configure_session_factory, session_factory
from extensions import ext_redis
@@ -102,3 +104,18 @@ def reset_secret_key():
yield
finally:
dify_config.SECRET_KEY = original
+
+
+@pytest.fixture(scope="session")
+def _unit_test_engine():
+ engine = create_engine("sqlite:///:memory:")
+ yield engine
+ engine.dispose()
+
+
+@pytest.fixture(autouse=True)
+def _configure_session_factory(_unit_test_engine):
+ try:
+ session_factory.get_session_maker()
+ except RuntimeError:
+ configure_session_factory(_unit_test_engine, expire_on_commit=False)
diff --git a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py
index 40eb59a8f4..c557605916 100644
--- a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py
+++ b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py
@@ -31,6 +31,13 @@ def _load_app_module():
def schema_model(self, name, schema):
self.models[name] = schema
+ return schema
+
+ def model(self, name, model_dict=None, **kwargs):
+ """Register a model with the namespace (flask-restx compatibility)."""
+ if model_dict is not None:
+ self.models[name] = model_dict
+ return model_dict
def _decorator(self, obj):
return obj
diff --git a/api/uv.lock b/api/uv.lock
index 7808c16a8c..a3ad292168 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -1479,6 +1479,7 @@ dev = [
{ name = "pytest-env" },
{ name = "pytest-mock" },
{ name = "pytest-timeout" },
+ { name = "pytest-xdist" },
{ name = "ruff" },
{ name = "scipy-stubs" },
{ name = "sseclient-py" },
@@ -1678,6 +1679,7 @@ dev = [
{ name = "pytest-env", specifier = "~=1.1.3" },
{ name = "pytest-mock", specifier = "~=3.14.0" },
{ name = "pytest-timeout", specifier = ">=2.4.0" },
+ { name = "pytest-xdist", specifier = ">=3.8.0" },
{ name = "ruff", specifier = "~=0.14.0" },
{ name = "scipy-stubs", specifier = ">=1.15.3.0" },
{ name = "sseclient-py", specifier = ">=1.8.0" },
@@ -1896,6 +1898,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/d8/2a1c638d9e0aa7e269269a1a1bf423ddd94267f1a01bbe3ad03432b67dd4/eval_type_backport-0.3.0-py3-none-any.whl", hash = "sha256:975a10a0fe333c8b6260d7fdb637698c9a16c3a9e3b6eb943fee6a6f67a37fe8", size = 6061, upload-time = "2025-11-13T20:56:49.499Z" },
]
+[[package]]
+name = "execnet"
+version = "2.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
+]
+
[[package]]
name = "faker"
version = "38.2.0"
@@ -5141,6 +5152,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
]
+[[package]]
+name = "pytest-xdist"
+version = "3.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "execnet" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
+]
+
[[package]]
name = "python-calamine"
version = "0.5.4"
diff --git a/dev/pytest/pytest_unit_tests.sh b/dev/pytest/pytest_unit_tests.sh
index 496cb40952..7c39a48bf4 100755
--- a/dev/pytest/pytest_unit_tests.sh
+++ b/dev/pytest/pytest_unit_tests.sh
@@ -5,6 +5,12 @@ SCRIPT_DIR="$(dirname "$(realpath "$0")")"
cd "$SCRIPT_DIR/../.."
PYTEST_TIMEOUT="${PYTEST_TIMEOUT:-20}"
+PYTEST_XDIST_ARGS="${PYTEST_XDIST_ARGS:--n auto}"
-# libs
-pytest --timeout "${PYTEST_TIMEOUT}" api/tests/unit_tests
+# Run most tests in parallel (excluding controllers which have import conflicts with xdist)
+# Controller tests have module-level side effects (Flask route registration) that cause
+# race conditions when imported concurrently by multiple pytest-xdist workers.
+pytest --timeout "${PYTEST_TIMEOUT}" ${PYTEST_XDIST_ARGS} api/tests/unit_tests --ignore=api/tests/unit_tests/controllers
+
+# Run controller tests sequentially to avoid import race conditions
+pytest --timeout "${PYTEST_TIMEOUT}" api/tests/unit_tests/controllers
diff --git a/web/AGENTS.md b/web/AGENTS.md
index 7362cd51db..5dd41b8a3c 100644
--- a/web/AGENTS.md
+++ b/web/AGENTS.md
@@ -1,5 +1,9 @@
+## Frontend Workflow
+
+- Refer to the `./docs/test.md` and `./docs/lint.md` for detailed frontend workflow instructions.
+
## Automated Test Generation
-- Use `web/testing/testing.md` as the canonical instruction set for generating frontend automated tests.
+- Use `./docs/test.md` as the canonical instruction set for generating frontend automated tests.
- When proposing or saving tests, re-read that document and follow every requirement.
- All frontend tests MUST also comply with the `frontend-testing` skill. Treat the skill as a mandatory constraint, not optional guidance.
diff --git a/web/README.md b/web/README.md
index 9c731a081a..64039709dc 100644
--- a/web/README.md
+++ b/web/README.md
@@ -107,6 +107,8 @@ Open [http://localhost:6006](http://localhost:6006) with your browser to see the
If your IDE is VSCode, rename `web/.vscode/settings.example.json` to `web/.vscode/settings.json` for lint code setting.
+Then follow the [Lint Documentation](./docs/lint.md) to lint the code.
+
## Test
We use [Vitest](https://vitest.dev/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for Unit Testing.
diff --git a/web/app/components/app-sidebar/toggle-button.tsx b/web/app/components/app-sidebar/toggle-button.tsx
index a6bdee4f78..cbfbeee452 100644
--- a/web/app/components/app-sidebar/toggle-button.tsx
+++ b/web/app/components/app-sidebar/toggle-button.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import Button from '../base/button'
import Tooltip from '../base/tooltip'
-import { getKeyboardKeyNameBySystem } from '../workflow/utils'
+import ShortcutsName from '../workflow/shortcuts-name'
type TooltipContentProps = {
expand: boolean
@@ -20,18 +20,7 @@ const TooltipContent = ({
return (
{expand ? t('sidebar.collapseSidebar', { ns: 'layout' }) : t('sidebar.expandSidebar', { ns: 'layout' })}
-
- {
- TOGGLE_SHORTCUT.map(key => (
-
- {getKeyboardKeyNameBySystem(key)}
-
- ))
- }
-
+
)
}
diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx
index 39cbb56d4c..1348e3111f 100644
--- a/web/app/components/app/app-publisher/index.tsx
+++ b/web/app/components/app/app-publisher/index.tsx
@@ -49,7 +49,8 @@ import Divider from '../../base/divider'
import Loading from '../../base/loading'
import Toast from '../../base/toast'
import Tooltip from '../../base/tooltip'
-import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils'
+import ShortcutsName from '../../workflow/shortcuts-name'
+import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
import AccessControl from '../app-access-control'
import PublishWithMultipleModel from './publish-with-multiple-model'
import SuggestedAction from './suggested-action'
@@ -342,13 +343,7 @@ const AppPublisher = ({
: (
{t('common.publishUpdate', { ns: 'workflow' })}
-
- {PUBLISH_SHORTCUT.map(key => (
-
- {getKeyboardKeyNameBySystem(key)}
-
- ))}
-
+
)
}
diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx
index e2b50cf030..66c7bce80c 100644
--- a/web/app/components/app/create-app-modal/index.tsx
+++ b/web/app/components/app/create-app-modal/index.tsx
@@ -1,7 +1,7 @@
'use client'
import type { AppIconSelection } from '../../base/app-icon-picker'
-import { RiArrowRightLine, RiArrowRightSLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react'
+import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks'
import Image from 'next/image'
@@ -29,6 +29,7 @@ import { getRedirection } from '@/utils/app-redirection'
import { cn } from '@/utils/classnames'
import { basePath } from '@/utils/var'
import AppIconPicker from '../../base/app-icon-picker'
+import ShortcutsName from '../../workflow/shortcuts-name'
type CreateAppProps = {
onSuccess: () => void
@@ -269,10 +270,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
{t('newApp.Cancel', { ns: 'app' })}
{t('newApp.Create', { ns: 'app' })}
-
-
-
-
+
diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx
index 838e9cc03f..04d8b1e754 100644
--- a/web/app/components/app/create-from-dsl-modal/index.tsx
+++ b/web/app/components/app/create-from-dsl-modal/index.tsx
@@ -1,7 +1,7 @@
'use client'
import type { MouseEventHandler } from 'react'
-import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
+import { RiCloseLine } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks'
import { noop } from 'es-toolkit/function'
import { useRouter } from 'next/navigation'
@@ -28,6 +28,7 @@ import {
} from '@/service/apps'
import { getRedirection } from '@/utils/app-redirection'
import { cn } from '@/utils/classnames'
+import ShortcutsName from '../../workflow/shortcuts-name'
import Uploader from './uploader'
type CreateFromDSLModalProps = {
@@ -298,10 +299,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
className="gap-1"
>
{t('newApp.Create', { ns: 'app' })}
-
-
-
-
+
diff --git a/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx b/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx
index efb9848494..a0cbfea147 100644
--- a/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx
+++ b/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx
@@ -4,7 +4,8 @@ import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
-import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
+import ShortcutsName from '@/app/components/workflow/shortcuts-name'
+import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
import { ChunkingMode } from '@/models/datasets'
import { useDocumentContext } from '../../context'
@@ -54,7 +55,7 @@ const ActionButtons: FC = ({
>
{t('operation.cancel', { ns: 'common' })}
- ESC
+
{(isParentChildParagraphMode && actionType === 'edit' && !isChildChunk && showRegenerationButton)
@@ -76,10 +77,7 @@ const ActionButtons: FC = ({
>
{t('operation.save', { ns: 'common' })}
-
- {getKeyboardKeyNameBySystem('ctrl')}
- S
-
+
diff --git a/web/app/components/explore/create-app-modal/index.tsx b/web/app/components/explore/create-app-modal/index.tsx
index 9bffcc6c69..cfe59fb7f3 100644
--- a/web/app/components/explore/create-app-modal/index.tsx
+++ b/web/app/components/explore/create-app-modal/index.tsx
@@ -1,6 +1,6 @@
'use client'
import type { AppIconType } from '@/types/app'
-import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
+import { RiCloseLine } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks'
import { noop } from 'es-toolkit/function'
import * as React from 'react'
@@ -17,6 +17,7 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { useProviderContext } from '@/context/provider-context'
import { AppModeEnum } from '@/types/app'
import AppIconPicker from '../../base/app-icon-picker'
+import ShortcutsName from '../../workflow/shortcuts-name'
export type CreateAppModalProps = {
show: boolean
@@ -198,10 +199,7 @@ const CreateAppModal = ({
onClick={handleSubmit}
>
{!isEditModal ? t('operation.create', { ns: 'common' }) : t('operation.save', { ns: 'common' })}
-
-
-
-
+
{t('operation.cancel', { ns: 'common' })}
diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx
index d34176e4c7..733e1d3162 100644
--- a/web/app/components/goto-anything/index.tsx
+++ b/web/app/components/goto-anything/index.tsx
@@ -12,7 +12,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
-import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common'
+import ShortcutsName from '@/app/components/workflow/shortcuts-name'
+import { getKeyboardKeyCodeBySystem, isEventTargetInputArea } from '@/app/components/workflow/utils/common'
import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation'
import { useGetLanguage } from '@/context/i18n'
import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace'
@@ -356,14 +357,7 @@ const GotoAnything: FC = ({
)}
-
-
- {isMac() ? '⌘' : 'Ctrl'}
-
-
- K
-
-
+
diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx
index 2912f13522..2dd56b4277 100644
--- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx
+++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx
@@ -28,11 +28,12 @@ import { useToastContext } from '@/app/components/base/toast'
import {
useChecklistBeforePublish,
} from '@/app/components/workflow/hooks'
+import ShortcutsName from '@/app/components/workflow/shortcuts-name'
import {
useStore,
useWorkflowStore,
} from '@/app/components/workflow/store'
-import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
+import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useDocLink } from '@/context/i18n'
import { useModalContextSelector } from '@/context/modal-context'
@@ -253,13 +254,7 @@ const Popup = () => {
: (
{t('common.publishUpdate', { ns: 'workflow' })}
-
- {PUBLISH_SHORTCUT.map(key => (
-
- {getKeyboardKeyNameBySystem(key)}
-
- ))}
-
+
)
}
diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx
index 00c531004f..81389e51b4 100644
--- a/web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx
+++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx
@@ -4,9 +4,9 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { useWorkflowRun, useWorkflowStartRun } from '@/app/components/workflow/hooks'
+import ShortcutsName from '@/app/components/workflow/shortcuts-name'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
-import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { cn } from '@/utils/classnames'
@@ -78,14 +78,7 @@ const RunMode = ({
)}
{
!isDisabled && (
-
-
- {getKeyboardKeyNameBySystem('alt')}
-
-
- R
-
-
+
)
}
diff --git a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx
index c483abfb0b..16bae51246 100644
--- a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx
+++ b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx
@@ -7,6 +7,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
+import ShortcutsName from '@/app/components/workflow/shortcuts-name'
import { BlockEnum } from '@/app/components/workflow/types'
import StartNodeSelectionPanel from './start-node-selection-panel'
@@ -75,9 +76,7 @@ const WorkflowOnboardingModal: FC = ({
{isShow && (
{t('onboarding.escTip.press', { ns: 'workflow' })}
-
- {t('onboarding.escTip.key', { ns: 'workflow' })}
-
+
{t('onboarding.escTip.toDismiss', { ns: 'workflow' })}
)}
diff --git a/web/app/components/workflow/header/run-mode.tsx b/web/app/components/workflow/header/run-mode.tsx
index 5c225156ac..63c48ff0dc 100644
--- a/web/app/components/workflow/header/run-mode.tsx
+++ b/web/app/components/workflow/header/run-mode.tsx
@@ -7,9 +7,9 @@ import { trackEvent } from '@/app/components/base/amplitude'
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { useToastContext } from '@/app/components/base/toast'
import { useWorkflowRun, useWorkflowRunValidation, useWorkflowStartRun } from '@/app/components/workflow/hooks'
+import ShortcutsName from '@/app/components/workflow/shortcuts-name'
import { useStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
-import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { cn } from '@/utils/classnames'
@@ -136,14 +136,7 @@ const RunMode = ({
>
{text ?? t('common.run', { ns: 'workflow' })}
-
-
- {getKeyboardKeyNameBySystem('alt')}
-
-
- R
-
-
+
)
diff --git a/web/app/components/workflow/header/version-history-button.tsx b/web/app/components/workflow/header/version-history-button.tsx
index 32e72dc184..b98dfeea76 100644
--- a/web/app/components/workflow/header/version-history-button.tsx
+++ b/web/app/components/workflow/header/version-history-button.tsx
@@ -8,7 +8,8 @@ import useTheme from '@/hooks/use-theme'
import { cn } from '@/utils/classnames'
import Button from '../../base/button'
import Tooltip from '../../base/tooltip'
-import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../utils'
+import ShortcutsName from '../shortcuts-name'
+import { getKeyboardKeyCodeBySystem } from '../utils'
type VersionHistoryButtonProps = {
onClick: () => Promise | unknown
@@ -23,16 +24,7 @@ const PopupContent = React.memo(() => {
{t('common.versionHistory', { ns: 'workflow' })}
-
- {VERSION_HISTORY_SHORTCUT.map(key => (
-
- {getKeyboardKeyNameBySystem(key)}
-
- ))}
-
+
)
})
diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/advanced-actions.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/advanced-actions.tsx
index 536277b9e2..8aad824008 100644
--- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/advanced-actions.tsx
+++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/advanced-actions.tsx
@@ -3,7 +3,8 @@ import { useKeyPress } from 'ahooks'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
-import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
+import ShortcutsName from '@/app/components/workflow/shortcuts-name'
+import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
type AdvancedActionsProps = {
isConfirmDisabled: boolean
@@ -11,15 +12,6 @@ type AdvancedActionsProps = {
onConfirm: () => void
}
-const Key = (props: { keyName: string }) => {
- const { keyName } = props
- return (
-
- {keyName}
-
- )
-}
-
const AdvancedActions: FC = ({
isConfirmDisabled,
onCancel,
@@ -48,10 +40,7 @@ const AdvancedActions: FC = ({
onClick={onConfirm}
>
{t('operation.confirm', { ns: 'common' })}
-
-
-
-
+
)
diff --git a/web/app/components/workflow/shortcuts-name.tsx b/web/app/components/workflow/shortcuts-name.tsx
index d0ce007f61..3d21cff316 100644
--- a/web/app/components/workflow/shortcuts-name.tsx
+++ b/web/app/components/workflow/shortcuts-name.tsx
@@ -6,11 +6,13 @@ type ShortcutsNameProps = {
keys: string[]
className?: string
textColor?: 'default' | 'secondary'
+ bgColor?: 'gray' | 'white'
}
const ShortcutsName = ({
keys,
className,
textColor = 'default',
+ bgColor = 'gray',
}: ShortcutsNameProps) => {
return (
diff --git a/web/docs/lint.md b/web/docs/lint.md
new file mode 100644
index 0000000000..051f9e6ecd
--- /dev/null
+++ b/web/docs/lint.md
@@ -0,0 +1,51 @@
+# Lint Guide
+
+We use ESLint and Typescript to maintain code quality and consistency across the project.
+
+## ESLint
+
+### Common Flags
+
+**File/folder targeting**: Append paths to lint specific files or directories.
+
+```sh
+pnpm eslint [options] file.js [file.js] [dir]
+```
+
+**`--cache`**: Caches lint results for faster subsequent runs. Keep this enabled by default; only disable when you encounter unexpected lint results.
+
+**`--concurrency`**: Enables multi-threaded linting. Use `--concurrency=auto` or experiment with specific numbers to find the optimal setting for your machine. Keep this enabled when linting multiple files.
+
+- [ESLint multi-thread linting blog post](https://eslint.org/blog/2025/08/multithread-linting/)
+
+**`--fix`**: Automatically fixes auto-fixable rule violations. Always review the diff before committing to ensure no unintended changes.
+
+**`--quiet`**: Suppresses warnings and only shows errors. Useful when you want to reduce noise from existing issues.
+
+**`--suppress-all`**: Temporarily suppresses error-level violations and records them, allowing CI to pass. Treat this as an escape hatch—fix these errors when time permits.
+
+**`--prune-suppressions`**: Removes outdated suppressions after you've fixed the underlying errors.
+
+- [ESLint bulk suppressions blog post](https://eslint.org/blog/2025/04/introducing-bulk-suppressions/)
+
+### Type-Aware Linting
+
+Some ESLint rules require type information, such as [no-leaked-conditional-rendering](https://www.eslint-react.xyz/docs/rules/no-leaked-conditional-rendering). However, [typed linting via typescript-eslint](https://typescript-eslint.io/getting-started/typed-linting) is too slow for practical use, so we use [TSSLint](https://github.com/johnsoncodehk/tsslint) instead.
+
+```sh
+pnpm lint:tss
+```
+
+This command lints the entire project and is intended for final verification before committing or pushing changes.
+
+## Type Check
+
+You should be able to see suggestions from TypeScript in your editor for all open files.
+
+However, it can be useful to run the TypeScript 7 command-line (tsgo) to type check all files:
+
+```sh
+pnpm type-check:tsgo
+```
+
+Prefer using `tsgo` for type checking as it is significantly faster than the standard TypeScript compiler. Only fall back to `pnpm type-check` (which uses `tsc`) if you encounter unexpected results.
diff --git a/web/testing/testing.md b/web/docs/test.md
similarity index 99%
rename from web/testing/testing.md
rename to web/docs/test.md
index 47341e445e..cac0e0e351 100644
--- a/web/testing/testing.md
+++ b/web/docs/test.md
@@ -360,11 +360,11 @@ describe('ComponentName', () => {
let mockPortalOpenState = false
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
- PortalToFollowElem: ({ children, open, ...props }: any) => {
+ PortalToFollowElem: ({ children, open, ...props }) => {
mockPortalOpenState = open || false // Update shared state
return
{children}
},
- PortalToFollowElemContent: ({ children }: any) => {
+ PortalToFollowElemContent: ({ children }) => {
// ✅ Matches actual: returns null when open is false
if (!mockPortalOpenState)
return null
diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json
index 965ac492fb..2662e979c1 100644
--- a/web/eslint-suppressions.json
+++ b/web/eslint-suppressions.json
@@ -4371,11 +4371,6 @@
"count": 10
}
},
- "testing/testing.md": {
- "ts/no-explicit-any": {
- "count": 2
- }
- },
"types/app.ts": {
"ts/no-explicit-any": {
"count": 1
diff --git a/web/scripts/analyze-component.js b/web/scripts/analyze-component.js
index b09301503c..2fdff2f3d0 100755
--- a/web/scripts/analyze-component.js
+++ b/web/scripts/analyze-component.js
@@ -337,7 +337,7 @@ Test file under review:
${testPath}
Checklist (ensure every item is addressed in your review):
-- Confirm the tests satisfy all requirements listed above and in web/testing/TESTING.md.
+- Confirm the tests satisfy all requirements listed above and in web/docs/test.md.
- Verify Arrange → Act → Assert structure, mocks, and cleanup follow project conventions.
- Ensure all detected component features (state, effects, routing, API, events, etc.) are exercised, including edge cases and error paths.
- Check coverage of prop variations, null/undefined inputs, and high-priority workflows implied by usage score.
@@ -382,7 +382,7 @@ Examples:
# Review existing test
pnpm analyze-component app/components/base/button/index.tsx --review
-For complete testing guidelines, see: web/testing/testing.md
+For complete testing guidelines, see: web/docs/test.md
`)
}