diff --git a/.agents/skills/frontend-code-review/references/component-architecture.md b/.agents/skills/frontend-code-review/references/component-architecture.md index ffabbf4cd69..9eda6589fd0 100644 --- a/.agents/skills/frontend-code-review/references/component-architecture.md +++ b/.agents/skills/frontend-code-review/references/component-architecture.md @@ -61,7 +61,7 @@ Flag effects that: - Transform props/state for rendering. - Copy one state value into another representing the same concept. - Handle user actions that belong in event handlers. -- Reset state from props when a keyed reset, stable ID, or render-time derivation would work. +- Reset local state from props or visibility when derivation, a stable semantic identity, or the intended mounted owner already expresses the lifecycle. - Fetch data that belongs in framework APIs or TanStack Query. If an effect remains, it must synchronize with a named external system: browser API, subscription, timer, analytics-on-visibility, non-React widget, or imperative DOM integration. @@ -71,11 +71,24 @@ If an effect remains, it must synchronize with a named external system: browser Flag: - Storing derived booleans, disabled flags, default tabs, or loading labels that can be calculated from current query/feature state. +- Per-session state held by a longer-lived visibility coordinator and cleared through an open-state Effect or a generated key when the primitive's mounted-content lifecycle already matches the intended state lifetime. +- A DOM field mirrored into competing prop, default, and React state sources when editing does not require those sources to synchronize. - Local state used to fake server data or generated contract fields. - UI state persisted to localStorage when it is live app state. - Feature-local mock shells wired to unrelated existing APIs before the real API is confirmed. -Prefer render-time derivation. Keep true local state for user choices, transient input, controlled popups, and feature UI state that has no server source. +Review state lifetime before its storage mechanism. For a hidden surface, distinguish the +visibility coordinator from mounted content. State private to one mounted session belongs in that +content owner; promote it only when the draft must survive that content owner's unmount or another +owner coordinates it. A stable semantic identity key may create a new snapshot when the represented +identity changes; a generated key is not a routine reset command. + +Prefer render-time derivation. Keep true local state for user choices, transient input, controlled +popups, and feature UI state that has no server source. Submit-only DOM fields may remain +uncontrolled; use local controlled state when React must own the current value to drive rendering +or coordination. Observing change events or tracking a derived fact such as dirty state does not +require mirroring the field value. Do not flag controlled state by itself without a concrete +competing-source, stale-state, or ownership defect. ## Navigation diff --git a/.agents/skills/frontend-code-review/references/testing.md b/.agents/skills/frontend-code-review/references/testing.md index 2f81d10589e..3bda124c01b 100644 --- a/.agents/skills/frontend-code-review/references/testing.md +++ b/.agents/skills/frontend-code-review/references/testing.md @@ -9,6 +9,7 @@ Flag missing coverage when a change alters a reachable contract such as: - User interaction, navigation, form submission, validation, or permissions. - Query or mutation behavior, URL state, persistence, or one-shot signals. - Loading, error, empty, and recovery states that users can encounter. +- A hidden surface whose close-and-reopen behavior changes whether in-progress state resets or persists. - Accessibility-critical labels, keyboard flow, focus, disabled state, or overlay behavior. - A regression-prone business rule or bug fix that can be reproduced through a public boundary. diff --git a/.agents/skills/how-to-write-component/SKILL.md b/.agents/skills/how-to-write-component/SKILL.md index 2b0e50f538d..b6599743227 100644 --- a/.agents/skills/how-to-write-component/SKILL.md +++ b/.agents/skills/how-to-write-component/SKILL.md @@ -9,10 +9,11 @@ Use this skill to route component architecture decisions to its bundled referenc ## First Decisions -| Question | Default | Promote only when | +| Question | Default | Choose differently when | | --- | --- | --- | | Where should code live? | In the product workflow, route, or feature owner. | Several verticals need the same stable contract. | -| Who owns state and handlers? | The lowest visual owner that consumes them. | A parent coordinates one workflow or consistent snapshot. | +| Who owns state and handlers? | The lowest owner that consumes them and whose lifetime matches the state. | Another owner coordinates the value or it must survive the local owner's unmount. | +| Should React control a value? | Leave submit-only DOM fields uncontrolled. | The workflow must own the current value to drive rendering or coordination. | | Should state enter Jotai? | Keep component and form state local. | Siblings need one source of truth or scoped workflow persistence. | | Who owns URL state? | Next.js route APIs and `nuqs`. | Atoms require a read-only route-identity bridge. | | Who owns remote state? | TanStack Query at the lowest consumer. | Atom state drives the query or shared derivations consume it. | @@ -24,12 +25,12 @@ Use this skill to route component architecture decisions to its bundled referenc - Component moves, module boundaries, props, types, or owner placement: read [`references/ownership.md`][ownership]. - Jotai, form drafts, route identity, URL state, or persistence: read [`references/state.md`][state]. - Generated contracts, nullable API data, Query, mutations, SSR, auth, or workspace state: read [`references/data.md`][data]. -- Hotkeys, focus, dialogs, menus, popovers, or other secondary surfaces: read [`references/interactions.md`][interactions] and the overlay guide it references when applicable. +- Hotkeys, focus, dialogs, menus, popovers, or other secondary surfaces: read [`references/interactions.md`][interactions] and the overlay guide it references when applicable. Also read [`references/state.md`][state] when the surface owns a draft or other local session state. - Effects, navigation, memoization, preloading, or render cost: read [`references/runtime.md`][runtime]. ## Workflow -1. Identify the behavior owner and the public contract being changed. +1. Identify the behavior owner, the required state lifetime, and the public contract being changed. 2. Read the nearby implementation, tests, and only the routed skill references. 3. Implement one coherent vertical slice. Do not expand into equivalent patterns elsewhere unless the current contract cannot be completed without them. 4. Verify observable behavior at the narrowest sufficient boundary, then run the checks documented by the owning package: `web/docs/test.md` or `web/docs/lint.md` for Web, and `packages/dify-ui/docs/testing.md` for Dify UI. diff --git a/.agents/skills/how-to-write-component/references/interactions.md b/.agents/skills/how-to-write-component/references/interactions.md index 68233987b61..ba9315f981e 100644 --- a/.agents/skills/how-to-write-component/references/interactions.md +++ b/.agents/skills/how-to-write-component/references/interactions.md @@ -23,8 +23,10 @@ Read this document when a change involves application hotkeys, focus, dialogs, m - Follow the [overlay contract] for primitive choice and shared mechanics. The nearest consumer `AGENTS.md` owns application-specific composite reuse policy. - Separate behavior ownership from placement ownership: the action may own trigger, open state, and menu content while the caller owns slots, offsets, and alignment. - Keep menu and dialog surfaces as siblings when a menu command opens a dialog. Mount the dialog outside popup content. -- Mount controlled overlays unconditionally unless unmounting is required for performance or reset semantics. Prefer keyed or owner-local reset over conditional wrappers. -- Put query and mutation work inside dialog or alert-dialog content when it should mount only after opening. -- Prefer uncontrolled roots when the primitive can own open state. Use controlled state only for business coordination, analytics, cleanup, or explicit reset behavior. +- Keep overlay open-state ownership separate from content-session ownership. A controlled root does not require controlled fields or root-owned drafts. +- Match transient state to the primitive's content mount lifecycle. State below an unmounting content boundary gets a fresh instance after unmount; intentionally kept-mounted content needs an explicit persistence or reset policy. +- Keep a controlled overlay root at its coordination owner so the primitive can complete exit transitions, focus restoration, and detached-handle behavior. Do not conditionally remove the root to reset content state, and use keys only for stable semantic identity. +- Place query subscriptions and mutation observers at the owner whose lifetime matches when they should run. Mounted-session work may belong inside content; work that must start or stop exactly with `open` needs an explicit open-state condition. +- Prefer primitive-owned open state unless another owner must observe or coordinate it. Analytics callbacks and local cleanup alone do not require a controlled root. [overlay contract]: ../../../../packages/dify-ui/docs/overlays.md diff --git a/.agents/skills/how-to-write-component/references/ownership.md b/.agents/skills/how-to-write-component/references/ownership.md index d8da521aafc..886421ef394 100644 --- a/.agents/skills/how-to-write-component/references/ownership.md +++ b/.agents/skills/how-to-write-component/references/ownership.md @@ -12,8 +12,8 @@ Read this document when adding, moving, splitting, or refactoring React componen ## Component Ownership -- Put state, data access, loading, empty, error, and handlers in the lowest visual owner that uses them. -- Keep coordination in a parent only when it needs one consistent snapshot or coordinates submission, shared selection, batch behavior, navigation, or cross-section loading and errors. +- Put state, data access, loading, empty, error, and handlers in the lowest owner that uses them and whose mounted lifetime matches the required persistence. +- Keep coordination in a parent only when it needs one consistent snapshot, the value must intentionally survive the local owner's unmount, or the parent coordinates submission, shared selection, batch behavior, navigation, or cross-section loading and errors. - Repeated TanStack Query calls in siblings are acceptable when each sibling independently consumes the data; the cache already deduplicates requests. - Pass stable domain identity across boundaries. Do not pass raw server data together with separately derived flags for the same concept. - One pass-through prop layer is acceptable. Repeated forwarding means ownership should move closer to the consumer or into feature-scoped shared state. @@ -23,7 +23,8 @@ Read this document when adding, moving, splitting, or refactoring React componen ## Boundaries - State-heavy wizards, drawers, modals, and secondary workflows can form a small vertical surface with an entrypoint, optional feature-local state, and shallow owners matching real visual regions. -- The entrypoint owns route integration, provider wiring, close behavior, and mounting. Composition owners handle workflow branches; the closest visual owner handles section branches. +- The entrypoint owns route integration, provider wiring, placement, and open-state coordination. A content or session owner keeps state scoped to that mounted surface. +- Judge hook lifetime by the component that declares the hook and the primitive's mount contract, not only by where its rendered controls appear in JSX. - Separate hidden dialogs, dropdowns, and popovers into small local owners when their content obscures the parent flow. - Keep cohesive forms, menu bodies, and one-off helpers local unless they have their own state, reuse, or semantic boundary. - Avoid wrapper components and wrapper DOM that only rename props, pass children through, or hide the real primitive. A wrapper must own behavior, validation, state, accessibility, layout, or library integration. diff --git a/.agents/skills/how-to-write-component/references/runtime.md b/.agents/skills/how-to-write-component/references/runtime.md index 24554d895f5..42131944637 100644 --- a/.agents/skills/how-to-write-component/references/runtime.md +++ b/.agents/skills/how-to-write-component/references/runtime.md @@ -7,7 +7,7 @@ Read this document when a change introduces Effects, navigation side effects, me - Keep render pure: do not read or write `ref.current` during render except for predictable null-guarded lazy initialization. Update interaction-owned refs in event handlers, synchronize external-system refs after commit, and use state or derivation for rendered values. - Use Effects only to synchronize with a named external system such as a browser API, subscription, timer, analytics integration, non-React widget, or imperative DOM API. - Do not use Effects to transform render state, handle user actions, copy query data, reset state from props, or fetch data owned by framework APIs or TanStack Query. -- Initialize query-backed forms with keyed remounts or surface-entry hydration instead of copying data through Effects. +- Initialize query-backed form sessions after their defaults are available instead of copying data through Effects. Use a stable semantic identity key when the represented identity changes; use the intended surface lifecycle for per-session reset. ## Navigation diff --git a/.agents/skills/how-to-write-component/references/state.md b/.agents/skills/how-to-write-component/references/state.md index 0ede0a85e92..680d77fb8b9 100644 --- a/.agents/skills/how-to-write-component/references/state.md +++ b/.agents/skills/how-to-write-component/references/state.md @@ -9,10 +9,12 @@ Read this document when a change involves Jotai, form drafts, route identity, sh - Keep server and cache state in TanStack Query. Use existing feature stores for complex, high-frequency interaction state such as workflow canvas drag, resize, and runtime panels. - Use feature-owned storage only for low-frequency client preferences, dismissed notices, and UI defaults. Live application state does not belong in local storage. -## Forms +## Forms And Sessions -- Prefer uncontrolled Dify UI form and field controls when values are only read at submit time. Initialize query-backed defaults with `defaultValue` and keyed remounts. -- Promote form values to atoms only when another owner reacts to in-progress values, the draft must survive scoped unmounting, or several workflow steps edit the same draft. +- Keep form state in the narrowest owner whose lifetime matches the draft. A draft scoped to one mounted surface belongs to that content or session owner; a draft that must survive its current owner's unmount belongs to an explicit longer-lived feature owner. +- Prefer uncontrolled fields when values are only read at submit time. Use local controlled state only when React must own the current value to drive dependent UI or linked fields; track derived facts such as dirty state without mirroring the field value. Controlledness does not decide whether a draft is local or persisted. +- For query-backed defaults, establish the form session after the required defaults are available. `defaultValue` initializes the current mount; a stable semantic identity key may create a fresh snapshot when the represented identity changes. Do not use a generated key as a routine reset command. +- Promote drafts beyond the session only when another owner reacts to in-progress values, several workflow steps share one draft, or the draft must intentionally survive unmounting. Start with the lowest shared React owner; use feature-scoped atoms only when their coordination or persistence contract is needed. - Keep validation, source priority, fallback behavior, dirty checks, and payload assembly in the workflow that owns submission. ## Route And URL State diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3998a69c36a..05712d24189 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,7 +12,7 @@ "features": { "ghcr.io/devcontainers/features/node:1": { "nodeGypDependencies": true, - "version": "lts" + "version": "24.20.0" }, "ghcr.io/devcontainers-extra/features/npm-package:1": { "package": "typescript", @@ -46,4 +46,4 @@ // Configure tool-specific properties. // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. -} \ No newline at end of file +} diff --git a/.github/actions/setup-web/action.yml b/.github/actions/setup-web/action.yml index 68c330d0e2c..dbc20df50af 100644 --- a/.github/actions/setup-web/action.yml +++ b/.github/actions/setup-web/action.yml @@ -11,6 +11,6 @@ runs: - name: Setup Vite+ uses: voidzero-dev/setup-vp@1b32467adbe183473499fd9d5d372c3ed9641754 # v1.18.0 with: - node-version-file: .nvmrc + node-version-file: package.json cache: true run-install: true diff --git a/.github/labeler.yml b/.github/labeler.yml index c2ea8873781..911aa34e37a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -6,7 +6,6 @@ web: - 'package.json' - 'pnpm-lock.yaml' - 'pnpm-workspace.yaml' - - '.nvmrc' e2e: - changed-files: diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 7ad86aa41b8..6023aa7fae2 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -82,7 +82,6 @@ jobs: - 'pnpm-workspace.yaml' - 'lint.config.ts' - '.npmrc' - - '.nvmrc' - '.github/workflows/cli-tests.yml' - '.github/actions/setup-web/**' web: @@ -91,7 +90,6 @@ jobs: - 'package.json' - 'pnpm-lock.yaml' - 'pnpm-workspace.yaml' - - '.nvmrc' - '.github/workflows/main-ci.yml' - '.github/workflows/web-tests.yml' - '.github/actions/setup-web/**' @@ -105,7 +103,6 @@ jobs: - 'package.json' - 'pnpm-lock.yaml' - 'pnpm-workspace.yaml' - - '.nvmrc' - 'docker/docker-compose.middleware.yaml' - 'docker/envs/middleware.env.example' - '.github/workflows/web-e2e.yml' diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml index de1fce7977c..8f410b887be 100644 --- a/.github/workflows/post-merge.yml +++ b/.github/workflows/post-merge.yml @@ -39,7 +39,6 @@ jobs: - 'e2e/tsx-register.js' - 'package.json' - 'pnpm-lock.yaml' - - '.nvmrc' - '.github/workflows/post-merge.yml' - '.github/workflows/web-e2e.yml' - '.github/actions/setup-web/**' diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index e2dc5504205..ca9d93d136e 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -114,7 +114,6 @@ jobs: pnpm-workspace.yaml knip.config.ts scripts/check-web-production-unused-after-knip-fix.mjs - .nvmrc .github/workflows/style.yml .github/actions/setup-web/** @@ -170,7 +169,6 @@ jobs: package.json pnpm-lock.yaml pnpm-workspace.yaml - .nvmrc vite.config.ts lint.config.ts eslint.config.mjs diff --git a/.github/workflows/tool-test-sdks.yaml b/.github/workflows/tool-test-sdks.yaml index 2d0133131eb..e6ab2459edf 100644 --- a/.github/workflows/tool-test-sdks.yaml +++ b/.github/workflows/tool-test-sdks.yaml @@ -31,7 +31,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 22 + node-version-file: package.json cache: '' cache-dependency-path: 'pnpm-lock.yaml' diff --git a/.github/workflows/translate-i18n-claude.yml b/.github/workflows/translate-i18n-claude.yml index 20446914e6e..e24e98f356f 100644 --- a/.github/workflows/translate-i18n-claude.yml +++ b/.github/workflows/translate-i18n-claude.yml @@ -162,7 +162,7 @@ jobs: - name: Run Claude Code for Translation Sync if: steps.context.outputs.CHANGED_FILES != '' - uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1.0.210 + uses: anthropics/claude-code-action@833fb0f8c9f6686b33d963a8bae0a94f4936ab2a # v1.0.211 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 2bd5a0a98a3..00000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/AGENTS.md b/AGENTS.md index 6f5673b316a..d7b7320aa2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,3 +7,9 @@ Dify is an open-source platform for building LLM applications, agentic workflows - Run backend commands through `uv run --project api `. - Backend integration tests are CI-only and are not expected to run locally. - Keep `docker/.env.example` limited to variables required for a default Docker Compose deployment to start. Put optional and provider-specific settings in the matching `docker/envs/*.env.example` file; `docker/.env` overrides those service-specific env files. + +## Frontend Workflow + +- For truncated text disclosure and native `title` decisions, follow [Truncated Text Disclosure]. + +[Truncated Text Disclosure]: web/docs/truncated-text-disclosure.md diff --git a/api/Dockerfile b/api/Dockerfile index 86eef5d329f..7c9b5d1a158 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -53,15 +53,14 @@ WORKDIR /app/api # Create non-root user ARG dify_uid=1001 -ARG NODE_MAJOR=22 -ARG NODE_PACKAGE_VERSION=22.21.0-1nodesource1 +ARG NODE_PACKAGE_VERSION=24.20.0-1nodesource1 ARG NODESOURCE_KEY_FPR=6F71F525282841EEDAF851B42F59B5F99B1BE0B4 RUN groupadd -r -g ${dify_uid} dify && \ useradd -r -u ${dify_uid} -g ${dify_uid} -s /bin/bash dify && \ chown -R dify:dify /app -RUN \ - apt-get update \ +RUN NODE_MAJOR="${NODE_PACKAGE_VERSION%%.*}" \ + && apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index 730dd7b1a57..f95f9d8b00f 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -7,7 +7,6 @@ from pydantic import AliasChoices, BaseModel, Field, field_validator from sqlalchemy import func, or_, select from sqlalchemy.orm import Session -from configs import dify_config from controllers.common.schema import ( query_params_from_model, query_params_from_request, @@ -79,11 +78,9 @@ from services.agent.observability_service import ( ) from services.agent.roster_service import AgentRosterService from services.app_service import AgentAppPublicationCounts, AppListParams, AppService, CreateAppParams -from services.enterprise import rbac_service as enterprise_rbac_service from services.enterprise.enterprise_service import EnterpriseService from services.entities.agent_entities import ComposerSavePayload, RosterListQuery from services.feature_service import FeatureService -from tasks.initialize_created_app_rbac_access_task import initialize_created_app_rbac_access_task AgentPublicationStatus = Literal["published", "drafts"] @@ -687,15 +684,6 @@ class AgentAppListApi(Resource): ) app = AppService().create_app(current_tenant_id, params, current_user, session=session) - if dify_config.RBAC_ENABLED: - enterprise_rbac_service.RBACService.AppAccess.replace_whitelist( - current_tenant_id, - current_user.id, - str(app.id), - enterprise_rbac_service.ReplaceMemberBindings(automatic_include_workspace_members=True), - ) - initialize_created_app_rbac_access_task.delay(current_tenant_id, current_user.id, app_id=app.id) - return _serialize_agent_app_detail(session, app, current_user=current_user), 201 diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py index 3bbbecaecfd..094adc42022 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_datasets.py @@ -17,7 +17,7 @@ from controllers.console.wraps import ( with_current_user, ) from extensions.ext_database import db -from fields.dataset_fields import DatasetDetailResponse +from fields.dataset_fields import DatasetDetailResponse, dataset_detail_response_source from libs.helper import dump_response from libs.login import login_required from models import Account @@ -116,6 +116,7 @@ class CreateEmptyRagPipelineDatasetApi(Resource): # The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator if not current_user.is_dataset_editor: raise Forbidden() + session = db.session() dataset = DatasetService.create_empty_rag_pipeline_dataset( tenant_id=current_tenant_id, rag_pipeline_dataset_create_entity=RagPipelineDatasetCreateEntity( @@ -129,6 +130,6 @@ class CreateEmptyRagPipelineDatasetApi(Resource): permission=DatasetPermissionEnum.ONLY_ME, partial_member_list=None, ), - session=db.session(), + session=session, ) - return dump_response(DatasetDetailResponse, dataset), 201 + return dump_response(DatasetDetailResponse, dataset_detail_response_source(dataset, session=session)), 201 diff --git a/api/controllers/console/workspace/model_providers.py b/api/controllers/console/workspace/model_providers.py index ca0b536f85e..fe844204d27 100644 --- a/api/controllers/console/workspace/model_providers.py +++ b/api/controllers/console/workspace/model_providers.py @@ -1,7 +1,7 @@ import io from typing import Any, Literal -from flask import request, send_file +from flask import send_file from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import Session @@ -15,6 +15,7 @@ from controllers.console.wraps import ( RBACResourceScope, account_initialization_required, is_admin_or_owner_required, + model_validate, rbac_permission_required, setup_required, with_current_tenant_id, @@ -154,10 +155,8 @@ class ModelProviderListApi(Resource): @login_required @account_initialization_required @with_current_tenant_id - def get(self, tenant_id: str): - payload = request.args.to_dict(flat=True) - args = ParserModelList.model_validate(payload) - + @model_validate(ParserModelList) + def get(self, args: ParserModelList, tenant_id: str): model_provider_service = ModelProviderService() provider_list = model_provider_service.get_provider_list(tenant_id=tenant_id, model_type=args.model_type) @@ -212,12 +211,10 @@ class ModelProviderCredentialApi(Resource): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) @account_initialization_required @with_current_tenant_id - def get(self, tenant_id: str, provider: str): - # if credential_id is not provided, return current used credential - payload = request.args.to_dict(flat=True) - args = ParserCredentialId.model_validate(payload) - + @model_validate(ParserCredentialId) + def get(self, args: ParserCredentialId, tenant_id: str, provider: str): model_provider_service = ModelProviderService() + # if credential_id is not provided, return current used credential credentials = model_provider_service.get_provider_credential( tenant_id=tenant_id, provider=provider, credential_id=args.credential_id ) @@ -232,10 +229,8 @@ class ModelProviderCredentialApi(Resource): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False) @account_initialization_required @with_current_tenant_id - def post(self, current_tenant_id: str, provider: str): - payload = console_ns.payload or {} - args = ParserCredentialCreate.model_validate(payload) - + @model_validate(ParserCredentialCreate) + def post(self, args: ParserCredentialCreate, current_tenant_id: str, provider: str): model_provider_service = ModelProviderService() try: @@ -258,10 +253,8 @@ class ModelProviderCredentialApi(Resource): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) @account_initialization_required @with_current_tenant_id - def put(self, current_tenant_id: str, provider: str): - payload = console_ns.payload or {} - args = ParserCredentialUpdate.model_validate(payload) - + @model_validate(ParserCredentialUpdate) + def put(self, args: ParserCredentialUpdate, current_tenant_id: str, provider: str): model_provider_service = ModelProviderService() try: @@ -285,10 +278,8 @@ class ModelProviderCredentialApi(Resource): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False) @account_initialization_required @with_current_tenant_id - def delete(self, current_tenant_id: str, provider: str): - payload = console_ns.payload or {} - args = ParserCredentialDelete.model_validate(payload) - + @model_validate(ParserCredentialDelete) + def delete(self, args: ParserCredentialDelete, current_tenant_id: str, provider: str): model_provider_service = ModelProviderService() model_provider_service.remove_provider_credential( tenant_id=current_tenant_id, provider=provider, credential_id=args.credential_id @@ -307,10 +298,8 @@ class ModelProviderCredentialSwitchApi(Resource): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False) @account_initialization_required @with_current_tenant_id - def post(self, current_tenant_id: str, provider: str): - payload = console_ns.payload or {} - args = ParserCredentialSwitch.model_validate(payload) - + @model_validate(ParserCredentialSwitch) + def post(self, args: ParserCredentialSwitch, current_tenant_id: str, provider: str): service = ModelProviderService() service.switch_active_provider_credential( tenant_id=current_tenant_id, @@ -332,10 +321,8 @@ class ModelProviderValidateApi(Resource): @login_required @account_initialization_required @with_current_tenant_id - def post(self, current_tenant_id: str, provider: str): - payload = console_ns.payload or {} - args = ParserCredentialValidate.model_validate(payload) - + @model_validate(ParserCredentialValidate) + def post(self, args: ParserCredentialValidate, current_tenant_id: str, provider: str): tenant_id = current_tenant_id model_provider_service = ModelProviderService() @@ -388,10 +375,8 @@ class PreferredProviderTypeUpdateApi(Resource): @rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False) @account_initialization_required @with_current_tenant_id - def post(self, tenant_id: str, provider: str): - payload = console_ns.payload or {} - args = ParserPreferredProviderType.model_validate(payload) - + @model_validate(ParserPreferredProviderType) + def post(self, args: ParserPreferredProviderType, tenant_id: str, provider: str): model_provider_service = ModelProviderService() model_provider_service.switch_preferred_provider( tenant_id=tenant_id, provider=provider, preferred_provider_type=args.preferred_provider_type diff --git a/api/controllers/openapi/_errors.py b/api/controllers/openapi/_errors.py index 92884dfcd50..a53a379d948 100644 --- a/api/controllers/openapi/_errors.py +++ b/api/controllers/openapi/_errors.py @@ -67,6 +67,7 @@ class OpenApiErrorCode(StrEnum): MEMBER_LICENSE_EXCEEDED = "member_license_exceeded" HUMAN_INPUT_FORM_NOT_FOUND = "form_not_found" RECIPIENT_SURFACE_MISMATCH = "recipient_surface_mismatch" + TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE = "trigger_workflow_service_mode_unavailable" class ErrorDetail(BaseModel): diff --git a/api/controllers/openapi/app_run.py b/api/controllers/openapi/app_run.py index 772513ad417..631a750f0ee 100644 --- a/api/controllers/openapi/app_run.py +++ b/api/controllers/openapi/app_run.py @@ -35,6 +35,7 @@ from controllers.service_api.app.error import ( ProviderModelCurrentlyNotSupportError, ProviderNotInitializeError, ProviderQuotaExceededError, + TriggerWorkflowServiceModeUnavailableError, ) from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.app.apps.base_app_queue_manager import AppQueueManager @@ -57,6 +58,9 @@ from services.errors.app import ( WorkflowIdFormatError, WorkflowNotFoundError, ) +from services.errors.app import ( + TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError, +) from services.errors.llm import InvokeRateLimitError logger = logging.getLogger(__name__) @@ -70,6 +74,8 @@ def _translate_service_errors() -> Generator[None, None, None]: raise NotFound(str(ex)) except (IsDraftWorkflowError, WorkflowIdFormatError) as ex: raise BadRequest(str(ex)) + except TriggerWorkflowServiceModeUnavailableServiceError: + raise TriggerWorkflowServiceModeUnavailableError() except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") except services.errors.conversation.ConversationCompletedError: diff --git a/api/controllers/service_api/app/annotation.py b/api/controllers/service_api/app/annotation.py index 832190e08b2..0b54d133661 100644 --- a/api/controllers/service_api/app/annotation.py +++ b/api/controllers/service_api/app/annotation.py @@ -1,7 +1,6 @@ from typing import Literal from uuid import UUID -from flask import request from flask_restx import Resource from flask_restx.api import HTTPStatus from pydantic import BaseModel, Field, TypeAdapter @@ -207,9 +206,9 @@ class AnnotationListApi(Resource): ) @validate_app_token @with_session(write=False) - def get(self, session: Session, app_model: App): + @model_validate(AnnotationListQuery) + def get(self, query: AnnotationListQuery, session: Session, app_model: App): """List annotations for the application.""" - query = AnnotationListQuery.model_validate(request.args.to_dict(flat=True)) annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id( app_model.id, query.page, query.limit, query.keyword, session diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index 163a50e959b..124da072be9 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -2,7 +2,6 @@ from datetime import datetime from typing import Annotated, Any, Literal from uuid import UUID -from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, TypeAdapter, WithJsonSchema, field_validator from sqlalchemy.orm import sessionmaker @@ -185,7 +184,8 @@ class ConversationApi(Resource): service_api_ns.models[ConversationInfiniteScrollPagination.__name__], ) @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) - def get(self, app_model: App, end_user: EndUser): + @model_validate(ConversationListQuery) + def get(self, query_args: ConversationListQuery, app_model: App, end_user: EndUser): """List all conversations for the current user. Supports pagination using last_id and limit parameters. @@ -194,7 +194,6 @@ class ConversationApi(Resource): if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() - query_args = ConversationListQuery.model_validate(request.args.to_dict()) last_id = query_args.last_id or None try: @@ -343,7 +342,8 @@ class ConversationVariablesApi(Resource): service_api_ns.models[ConversationVariableInfiniteScrollPaginationResponse.__name__], ) @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) - def get(self, app_model: App, end_user: EndUser, conversation_id: UUID): + @model_validate(ConversationVariablesQuery) + def get(self, query_args: ConversationVariablesQuery, app_model: App, end_user: EndUser, conversation_id: UUID): """List all variables for a conversation. Conversational variables are only available for chat applications. @@ -355,7 +355,6 @@ class ConversationVariablesApi(Resource): conversation_id_str = str(conversation_id) - query_args = ConversationVariablesQuery.model_validate(request.args.to_dict()) last_id = query_args.last_id or None try: diff --git a/api/controllers/service_api/app/error.py b/api/controllers/service_api/app/error.py index e6f97e98249..60959746d49 100644 --- a/api/controllers/service_api/app/error.py +++ b/api/controllers/service_api/app/error.py @@ -1,4 +1,8 @@ from libs.exception import BaseHTTPException +from services.errors.app import ( + TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_CODE, + TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_MESSAGE, +) class AppUnavailableError(BaseHTTPException): @@ -37,6 +41,12 @@ class WorkflowVersionExecutionNotAllowedError(BaseHTTPException): code = 403 +class TriggerWorkflowServiceModeUnavailableError(BaseHTTPException): + error_code = TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_CODE + description = TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_MESSAGE + code = 403 + + class ConversationCompletedError(BaseHTTPException): error_code = "conversation_completed" description = "The conversation has ended. Please start a new conversation." diff --git a/api/controllers/service_api/app/file_preview.py b/api/controllers/service_api/app/file_preview.py index b315e190fa3..ad96ac96ce1 100644 --- a/api/controllers/service_api/app/file_preview.py +++ b/api/controllers/service_api/app/file_preview.py @@ -2,7 +2,7 @@ import logging from urllib.parse import quote from uuid import UUID -from flask import Response, request +from flask import Response from flask_restx import Resource from pydantic import BaseModel, Field from sqlalchemy import select @@ -10,6 +10,7 @@ from sqlalchemy import select from controllers.common.fields import BinaryFileResponse from controllers.common.file_response import enforce_download_for_html from controllers.common.schema import query_params_from_model, register_response_schema_model, register_schema_model +from controllers.console.wraps import model_validate from controllers.service_api import service_api_ns from controllers.service_api.app.error import ( FileAccessDeniedError, @@ -86,7 +87,8 @@ class FilePreviewApi(Resource): ) @service_api_ns.response(200, "File retrieved successfully") @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) - def get(self, app_model: App, end_user: EndUser, file_id: UUID): + @model_validate(FilePreviewQuery) + def get(self, args: FilePreviewQuery, app_model: App, end_user: EndUser, file_id: UUID): """ Preview/Download a file that was uploaded via Service API. @@ -95,9 +97,6 @@ class FilePreviewApi(Resource): """ file_id_str = str(file_id) - # Parse query parameters - args = FilePreviewQuery.model_validate(request.args.to_dict()) - # Validate file ownership and get file objects _, upload_file = self._validate_file_ownership(file_id_str, app_model.id) diff --git a/api/controllers/service_api/app/message.py b/api/controllers/service_api/app/message.py index d3443371313..63bf87709d9 100644 --- a/api/controllers/service_api/app/message.py +++ b/api/controllers/service_api/app/message.py @@ -2,7 +2,6 @@ import logging from typing import Annotated from uuid import UUID -from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, TypeAdapter, WithJsonSchema from werkzeug.exceptions import BadRequest, InternalServerError, NotFound @@ -102,7 +101,8 @@ class MessageListApi(Resource): service_api_ns.models[MessageInfiniteScrollPagination.__name__], ) @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) - def get(self, app_model: App, end_user: EndUser): + @model_validate(MessageListQuery) + def get(self, query_args: MessageListQuery, app_model: App, end_user: EndUser): """List messages in a conversation. Retrieves messages with pagination support using first_id. @@ -111,7 +111,6 @@ class MessageListApi(Resource): if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}: raise NotChatAppError() - query_args = MessageListQuery.model_validate(request.args.to_dict()) conversation_id = query_args.conversation_id first_id = query_args.first_id or None @@ -212,12 +211,12 @@ class AppGetFeedbacksApi(Resource): service_api_ns.models[AppFeedbackListResponse.__name__], ) @validate_app_token - def get(self, app_model: App): + @model_validate(FeedbackListQuery) + def get(self, query_args: FeedbackListQuery, app_model: App): """Get all feedbacks for the application. Returns paginated list of all feedback submitted for messages in this app. """ - query_args = FeedbackListQuery.model_validate(request.args.to_dict()) feedbacks = MessageService.get_all_messages_feedbacks( app_model, page=query_args.page, limit=query_args.limit, session=db.session() ) diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index 24f9fb7b62c..33a2f3a4b64 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -28,6 +28,7 @@ from controllers.service_api.app.error import ( ProviderModelCurrentlyNotSupportError, ProviderNotInitializeError, ProviderQuotaExceededError, + TriggerWorkflowServiceModeUnavailableError, WorkflowVersionExecutionNotAllowedError, ) from controllers.service_api.schema import ( @@ -61,7 +62,14 @@ from models.model import App, AppMode, EndUser from repositories.factory import DifyAPIRepositoryFactory from services.app_generate_service import AppGenerateService from services.billing_service import BillingService -from services.errors.app import IsDraftWorkflowError, WorkflowIdFormatError, WorkflowNotFoundError +from services.errors.app import ( + IsDraftWorkflowError, + WorkflowIdFormatError, + WorkflowNotFoundError, +) +from services.errors.app import ( + TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError, +) from services.errors.llm import InvokeRateLimitError from services.workflow_app_service import WorkflowAppService @@ -300,6 +308,11 @@ class WorkflowRunApi(Resource): "- `completion_request_error` : Workflow execution request failed.\n" "- `invalid_param` : Invalid parameter value." ), + 403: ( + "- `forbidden` : Token scope, app, or workspace access denied.\n" + "- `trigger_workflow_service_mode_unavailable` : Trigger-entry workflows cannot be invoked through " + "Web App, Service API, OpenAPI, or MCP." + ), 429: ( "- `too_many_requests` : Too many concurrent requests for this app.\n" "- `rate_limit_error` : The upstream model provider rate limit was exceeded." @@ -360,6 +373,8 @@ class WorkflowRunApi(Resource): # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) + except TriggerWorkflowServiceModeUnavailableServiceError: + raise TriggerWorkflowServiceModeUnavailableError() except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) except QuotaExceededError: @@ -406,8 +421,11 @@ class WorkflowRunByIdApi(Resource): "- `invalid_param` : Required parameter missing or invalid." ), 403: ( - "`workflow_version_execution_not_allowed` : Workflow version execution is unavailable on the " - "current plan. Upgrade to a paid plan." + "- `forbidden` : Token scope, app, or workspace access denied.\n" + "- `workflow_version_execution_not_allowed` : Workflow version execution is unavailable on the " + "current plan. Upgrade to a paid plan.\n" + "- `trigger_workflow_service_mode_unavailable` : The selected workflow version uses a trigger entry " + "and cannot be invoked through Web App, Service API, OpenAPI, or MCP." ), 404: "`not_found` : Workflow not found.", 429: ( @@ -487,6 +505,8 @@ class WorkflowRunByIdApi(Resource): # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) + except TriggerWorkflowServiceModeUnavailableServiceError: + raise TriggerWorkflowServiceModeUnavailableError() except WorkflowNotFoundError as ex: raise NotFound(str(ex)) except IsDraftWorkflowError as ex: diff --git a/api/controllers/web/error.py b/api/controllers/web/error.py index b0ab2f0334c..16253f06eee 100644 --- a/api/controllers/web/error.py +++ b/api/controllers/web/error.py @@ -1,4 +1,8 @@ from libs.exception import BaseHTTPException +from services.errors.app import ( + TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_CODE, + TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_MESSAGE, +) class AppUnavailableError(BaseHTTPException): @@ -31,6 +35,12 @@ class NotWorkflowAppError(BaseHTTPException): code = 400 +class TriggerWorkflowServiceModeUnavailableError(BaseHTTPException): + error_code = TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_CODE + description = TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_MESSAGE + code = 403 + + class ConversationCompletedError(BaseHTTPException): error_code = "conversation_completed" description = "The conversation has ended. Please start a new conversation." diff --git a/api/controllers/web/workflow.py b/api/controllers/web/workflow.py index 1e6d6e24d92..9dc728e2efd 100644 --- a/api/controllers/web/workflow.py +++ b/api/controllers/web/workflow.py @@ -14,6 +14,7 @@ from controllers.web.error import ( ProviderModelCurrentlyNotSupportError, ProviderNotInitializeError, ProviderQuotaExceededError, + TriggerWorkflowServiceModeUnavailableError, ) from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from controllers.web.wraps import WebApiResource @@ -30,6 +31,9 @@ from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from models.model import App, AppMode, EndUser from services.app_generate_service import AppGenerateService +from services.errors.app import ( + TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError, +) from services.errors.llm import InvokeRateLimitError logger = logging.getLogger(__name__) @@ -78,6 +82,8 @@ class WorkflowRunApi(WebApiResource): # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) + except TriggerWorkflowServiceModeUnavailableServiceError: + raise TriggerWorkflowServiceModeUnavailableError() except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) except QuotaExceededError: diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py index 7fd03788c7e..3b96c5da43e 100644 --- a/api/core/mcp/server/streamable_http.py +++ b/api/core/mcp/server/streamable_http.py @@ -12,6 +12,7 @@ from core.mcp import types as mcp_types from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.model import App, AppMCPServer, AppMode, EndUser from services.app_generate_service import AppGenerateService +from services.errors.app import TriggerWorkflowServiceModeUnavailableError logger = logging.getLogger(__name__) @@ -93,11 +94,16 @@ def handle_mcp_request( result=result_data.model_dump(by_alias=True, mode="json", exclude_none=True), ) - def create_error_response(code: int, message: str) -> mcp_types.JSONRPCError: + def create_error_response( + code: int, + message: str, + *, + data: Mapping[str, Any] | None = None, + ) -> mcp_types.JSONRPCError: """Create error response with error code and message""" from core.mcp.types import ErrorData - error_data = ErrorData(code=code, message=message) + error_data = ErrorData(code=code, message=message, data=data) return mcp_types.JSONRPCError( jsonrpc="2.0", id=request_id, @@ -131,6 +137,12 @@ def handle_mcp_request( case _: return create_error_response(mcp_types.METHOD_NOT_FOUND, f"Method not found: {request_type.__name__}") + except TriggerWorkflowServiceModeUnavailableError as e: + return create_error_response( + mcp_types.INVALID_REQUEST, + str(e), + data={"code": e.error_code}, + ) except ValueError as e: logger.exception("Invalid params") return create_error_response(mcp_types.INVALID_PARAMS, str(e)) diff --git a/api/models/dataset.py b/api/models/dataset.py index 891d949c8e6..ddcc8020ed8 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -212,17 +212,9 @@ class Dataset(Base): enable_api = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) is_multimodal = mapped_column(sa.Boolean, default=False, nullable=False, server_default=sa.text("false")) - @property - def total_documents(self) -> int: - return self.get_total_documents(session=db.session()) - def get_total_documents(self, *, session: Session) -> int: return self.get_document_count(session=session) - @property - def total_available_documents(self) -> int: - return self.get_total_available_documents(session=db.session()) - def get_total_available_documents(self, *, session: Session) -> int: return ( session.scalar( @@ -258,20 +250,12 @@ class Dataset(Base): def get_created_by_account(self, *, session: Session) -> Account | None: return session.get(Account, self.created_by) - @property - def author_name(self) -> str | None: - return self.get_author_name(session=db.session()) - def get_author_name(self, *, session: Session) -> str | None: account = self.get_created_by_account(session=session) if account: return account.name return None - @property - def latest_process_rule(self): - return self.get_latest_process_rule(session=db.session()) - def get_latest_process_rule(self, *, session: Session) -> "DatasetProcessRule | None": return session.scalar( select(DatasetProcessRule) @@ -391,10 +375,6 @@ class Dataset(Base): return tags or [] - @property - def external_knowledge_info(self) -> dict[str, Any] | None: - return self.get_external_knowledge_info(session=db.session()) - def get_external_knowledge_info(self, *, session: Session) -> dict[str, Any] | None: if self.provider != "external": return None @@ -974,17 +954,15 @@ class DocumentSegment(TypeBase): """Load the owning document with the caller-owned database session.""" return session.get(Document, self.document_id) - @property - def previous_segment(self): - return db.session.scalar( + def previous_segment(self, session: Session) -> "DocumentSegment | None": + return session.scalar( select(DocumentSegment).where( DocumentSegment.document_id == self.document_id, DocumentSegment.position == self.position - 1 ) ) - @property - def next_segment(self): - return db.session.scalar( + def next_segment(self, session: Session) -> "DocumentSegment | None": + return session.scalar( select(DocumentSegment).where( DocumentSegment.document_id == self.document_id, DocumentSegment.position == self.position + 1 ) @@ -1204,9 +1182,8 @@ class AppDatasetJoin(TypeBase): DateTime, nullable=False, server_default=sa.func.current_timestamp(), init=False ) - @property - def app(self): - return db.session.get(App, self.app_id) + def app(self, session: Session) -> App | None: + return session.get(App, self.app_id) class DatasetQuery(TypeBase): diff --git a/api/openapi/markdown/service-openapi.md b/api/openapi/markdown/service-openapi.md index 80b5ab94db8..92a9f9a91de 100644 --- a/api/openapi/markdown/service-openapi.md +++ b/api/openapi/markdown/service-openapi.md @@ -2211,7 +2211,7 @@ Execute a workflow. Cannot be executed without a published workflow. | 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. | **application/json**: [WorkflowBlockingResponse](#workflowblockingresponse)
**text/event-stream**: string
| | 400 | - `not_workflow_app` : App mode does not match the API route. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Workflow execution request failed. - `invalid_param` : Invalid parameter value. | | | 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 403 | - `forbidden` : Token scope, app, or workspace access denied. - `trigger_workflow_service_mode_unavailable` : Trigger-entry workflows cannot be invoked through Web App, Service API, OpenAPI, or MCP. | | | 404 | Workflow not found | | | 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | | 500 | `internal_server_error` : Internal server error. | | @@ -2287,7 +2287,7 @@ Execute a specific workflow version identified by its ID. Useful for running a p | 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. | **application/json**: [WorkflowBlockingResponse](#workflowblockingresponse)
**text/event-stream**: string
| | 400 | - `not_workflow_app` : App mode does not match the API route. - `bad_request` : Workflow is a draft or has an invalid ID format. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Workflow execution request failed. - `invalid_param` : Required parameter missing or invalid. | | | 401 | Unauthorized - invalid API token | | -| 403 | `workflow_version_execution_not_allowed` : Workflow version execution is unavailable on the current plan. Upgrade to a paid plan. | | +| 403 | - `forbidden` : Token scope, app, or workspace access denied. - `workflow_version_execution_not_allowed` : Workflow version execution is unavailable on the current plan. Upgrade to a paid plan. - `trigger_workflow_service_mode_unavailable` : The selected workflow version uses a trigger entry and cannot be invoked through Web App, Service API, OpenAPI, or MCP. | | | 404 | `not_found` : Workflow not found. | | | 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | | 500 | `internal_server_error` : Internal server error. | | diff --git a/api/services/app_generate_service.py b/api/services/app_generate_service.py index 4b271c6e94e..a22a7ba5d47 100644 --- a/api/services/app_generate_service.py +++ b/api/services/app_generate_service.py @@ -21,11 +21,17 @@ from core.app.features.rate_limiting import RateLimit from core.app.features.rate_limiting.rate_limit import rate_limit_context from core.app.layers.pause_state_persist_layer import PauseStateLayerConfig from core.db import session_factory +from core.trigger.constants import is_trigger_node_type from enums import DeploymentEdition, QuotaType from extensions.otel import AppGenerateHandler, trace_span from models.model import Account, App, AppMode, EndUser from models.workflow import Workflow, WorkflowRun -from services.errors.app import QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError +from services.errors.app import ( + QuotaExceededError, + TriggerWorkflowServiceModeUnavailableError, + WorkflowIdFormatError, + WorkflowNotFoundError, +) from services.errors.llm import InvokeRateLimitError from services.quota_service import QuotaService, unlimited from services.workflow_service import WorkflowService @@ -34,6 +40,13 @@ from tasks.app_generate.workflow_execute_task import AppExecutionParams, workflo logger = logging.getLogger(__name__) SSE_TASK_START_FALLBACK_MS = 200 +_MANUAL_WORKFLOW_INVOKE_SOURCES = frozenset( + { + InvokeFrom.OPENAPI, + InvokeFrom.SERVICE_API, + InvokeFrom.WEB_APP, + } +) if TYPE_CHECKING: from controllers.console.app.workflow import LoopNodeRunPayload @@ -290,6 +303,7 @@ class AppGenerateService: case AppMode.WORKFLOW: workflow_id = args.get("workflow_id") workflow = cls._get_workflow(app_model, invoke_from, workflow_id, session=session) + cls._ensure_workflow_service_mode_available(workflow=workflow, invoke_from=invoke_from) if streaming: with rate_limit_context(rate_limit, request_id): payload = AppExecutionParams.new( @@ -343,6 +357,16 @@ class AppGenerateService: case _: raise ValueError(f"Invalid app mode {app_model.mode}") + @staticmethod + def _ensure_workflow_service_mode_available(*, workflow: Workflow, invoke_from: InvokeFrom) -> None: + if invoke_from not in _MANUAL_WORKFLOW_INVOKE_SOURCES: + return + + for _, node_data in workflow.walk_nodes(): + node_type = node_data.get("type") + if isinstance(node_type, str) and is_trigger_node_type(node_type): + raise TriggerWorkflowServiceModeUnavailableError() + @staticmethod def _get_max_active_requests(app: App) -> int: """ diff --git a/api/services/enterprise/rbac_service.py b/api/services/enterprise/rbac_service.py index bf95b8650b1..fd0acf280b2 100644 --- a/api/services/enterprise/rbac_service.py +++ b/api/services/enterprise/rbac_service.py @@ -477,7 +477,6 @@ _LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS: list[str] = [ _LEGACY_APP_OWNER_KEYS: list[str] = [ "app.acl.preview", - "app.acl.access_point_manage", "app.acl.view_layout", "app.acl.test_and_run", "app.acl.edit", @@ -493,7 +492,6 @@ _LEGACY_APP_OWNER_KEYS: list[str] = [ _LEGACY_APP_ADMIN_KEYS: list[str] = [ "app.acl.preview", "app.acl.view_layout", - "app.acl.access_point_manage", "app.acl.test_and_run", "app.acl.edit", "app.acl.import_export_dsl", @@ -508,7 +506,6 @@ _LEGACY_APP_ADMIN_KEYS: list[str] = [ _LEGACY_APP_EDITOR_KEYS: list[str] = [ "app.acl.preview", - "app.acl.access_point_manage", "app.acl.view_layout", "app.acl.test_and_run", "app.acl.edit", diff --git a/api/services/errors/app.py b/api/services/errors/app.py index c9e9df97dea..74c29b0857d 100644 --- a/api/services/errors/app.py +++ b/api/services/errors/app.py @@ -18,6 +18,21 @@ class WorkflowIdFormatError(Exception): pass +TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_CODE = "trigger_workflow_service_mode_unavailable" +TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_MESSAGE = ( + "This workflow uses a trigger entry and cannot be invoked through Web App, Service API, OpenAPI, or MCP." +) + + +class TriggerWorkflowServiceModeUnavailableError(Exception): + """Raised when a trigger-entry Workflow is invoked through a manual service surface.""" + + error_code = TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_CODE + + def __init__(self) -> None: + super().__init__(TRIGGER_WORKFLOW_SERVICE_MODE_UNAVAILABLE_MESSAGE) + + class QuotaExceededError(ValueError): """Raised when billing quota is exceeded for a feature.""" diff --git a/api/tests/test_containers_integration_tests/models/test_dataset_models.py b/api/tests/test_containers_integration_tests/models/test_dataset_models.py index a3bbf196576..d817894b1de 100644 --- a/api/tests/test_containers_integration_tests/models/test_dataset_models.py +++ b/api/tests/test_containers_integration_tests/models/test_dataset_models.py @@ -49,7 +49,7 @@ class TestDatasetDocumentProperties: db_session_with_containers.add(doc) db_session_with_containers.flush() - assert dataset.total_documents == 3 + assert dataset.get_total_documents(session=db_session_with_containers) == 3 def test_dataset_available_documents_count(self, db_session_with_containers: Session) -> None: """Test dataset can count available documents.""" @@ -104,7 +104,7 @@ class TestDatasetDocumentProperties: db_session_with_containers.add_all([doc_available, doc_pending, doc_disabled]) db_session_with_containers.flush() - assert dataset.total_available_documents == 1 + assert dataset.get_total_available_documents(session=db_session_with_containers) == 1 def test_dataset_word_count_aggregation(self, db_session_with_containers: Session) -> None: """Test dataset can aggregate word count from documents.""" @@ -426,7 +426,7 @@ class TestDocumentSegmentNavigationProperties: db_session_with_containers.flush() # Act - prev_seg = segment.previous_segment + prev_seg = segment.previous_segment(session=db_session_with_containers) # Assert assert prev_seg is not None @@ -483,7 +483,7 @@ class TestDocumentSegmentNavigationProperties: db_session_with_containers.flush() # Act - next_seg = segment.next_segment + next_seg = segment.next_segment(session=db_session_with_containers) # Assert assert next_seg is not None diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index 27a543fd2a2..c2b43d8cb22 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -1,4 +1,3 @@ -from collections.abc import Callable from datetime import datetime from inspect import getsource, unwrap from types import SimpleNamespace @@ -309,15 +308,9 @@ def account_id() -> str: def test_agent_app_list_and_create_use_agent_route( - app: Flask, - monkeypatch: pytest.MonkeyPatch, - account_id: str, - sqlite_session: Session, - config_overrides: Callable[..., None], + app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str, sqlite_session: Session ) -> None: captured: dict[str, object] = {} - replace_whitelist = MagicMock() - initialize_access = MagicMock() class FakeAppService: def get_app(self, app_obj: object, *, session: object) -> object: @@ -403,9 +396,7 @@ def test_agent_app_list_and_create_use_agent_route( lambda _self, **kwargs: {"agent-list": "debug-conversation-list"}, ) monkeypatch.setattr( - roster_controller.AgentRosterService, - "count_agent_app_debug_conversation_messages", - lambda _self, **kwargs: 0, + roster_controller.AgentRosterService, "count_agent_app_debug_conversation_messages", lambda _self, **kwargs: 0 ) def get_or_create_debug_conversation(_self: object, **kwargs: object) -> str: @@ -422,13 +413,6 @@ def test_agent_app_list_and_create_use_agent_route( "get_system_features", lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)), ) - config_overrides(RBAC_ENABLED=True) - monkeypatch.setattr( - roster_controller.enterprise_rbac_service.RBACService.AppAccess, - "replace_whitelist", - replace_whitelist, - ) - monkeypatch.setattr(roster_controller.initialize_created_app_rbac_access_task, "delay", initialize_access) with app.test_request_context( "/console/api/agent?page=1&limit=10&mode=workflow&sort_by=recently_created" "&is_created_by_me=true&publication_status=published" @@ -469,22 +453,12 @@ def test_agent_app_list_and_create_use_agent_route( assert count_params.agent_is_published is True with app.test_request_context( "/console/api/agent", - json={ - "name": "Iris", - "description": "Agent app", - "role": "Coordinator", - "icon_type": "emoji", - "icon": "robot", - }, + json={"name": "Iris", "description": "Agent app", "role": "Coordinator", "icon_type": "emoji", "icon": "robot"}, ): created, status = unwrap(AgentAppListApi.post)( AgentAppListApi(), AgentAppCreatePayload( - name="Iris", - description="Agent app", - role="Coordinator", - icon_type="emoji", - icon="robot", + name="Iris", description="Agent app", role="Coordinator", icon_type="emoji", icon="robot" ), sqlite_session, "tenant-1", @@ -507,81 +481,6 @@ def test_agent_app_list_and_create_use_agent_route( "account_id": account_id, "commit": False, } - replace_whitelist.assert_called_once() - assert replace_whitelist.call_args.args[:3] == ("tenant-1", account_id, "app-created") - replace_payload = replace_whitelist.call_args.args[3] - assert replace_payload.automatic_include_workspace_members is True - initialize_access.assert_called_once_with("tenant-1", account_id, app_id="app-created") - - -def test_agent_app_create_skips_rbac_access_initialization_when_rbac_is_disabled( - app: Flask, - monkeypatch: pytest.MonkeyPatch, - account_id: str, - sqlite_session: Session, - config_overrides: Callable[..., None], -) -> None: - replace_whitelist = MagicMock() - initialize_access = MagicMock() - - class FakeAppService: - def get_app(self, app_obj: object, *, session: object) -> object: - return app_obj - - def create_app(self, tenant_id: str, params, current_user: object, *, session: object) -> object: - return _app_detail_obj(id="app-created", bound_agent_id="agent-created") - - monkeypatch.setattr(roster_controller, "AppService", FakeAppService) - monkeypatch.setattr( - roster_controller.AgentRosterService, - "get_app_backing_agent", - lambda _self, **kwargs: Agent( - id="agent-created", - app_id="app-created", - backing_app_id=None, - role="Created role", - active_config_snapshot_id=None, - ), - ) - monkeypatch.setattr( - roster_controller.AgentRosterService, - "get_or_create_build_conversation", - lambda _self, **kwargs: "debug-conversation-created", - ) - monkeypatch.setattr( - roster_controller.AgentRosterService, "count_agent_app_debug_conversation_messages", lambda _self, **kwargs: 0 - ) - monkeypatch.setattr( - roster_controller.FeatureService, - "get_system_features", - lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)), - ) - config_overrides(RBAC_ENABLED=False) - monkeypatch.setattr( - roster_controller.enterprise_rbac_service.RBACService.AppAccess, - "replace_whitelist", - replace_whitelist, - ) - monkeypatch.setattr(roster_controller.initialize_created_app_rbac_access_task, "delay", initialize_access) - - with app.test_request_context( - "/console/api/agent", - json={"name": "Iris", "description": "Agent app", "role": "Coordinator", "icon_type": "emoji", "icon": "robot"}, - ): - created, status = unwrap(AgentAppListApi.post)( - AgentAppListApi(), - AgentAppCreatePayload( - name="Iris", description="Agent app", role="Coordinator", icon_type="emoji", icon="robot" - ), - sqlite_session, - "tenant-1", - _account(account_id=account_id), - ) - - assert status == 201 - assert created["id"] == "agent-created" - replace_whitelist.assert_not_called() - initialize_access.assert_not_called() def test_agent_app_create_payload_allows_optional_role() -> None: diff --git a/api/tests/unit_tests/controllers/console/datasets/test_datasets.py b/api/tests/unit_tests/controllers/console/datasets/test_datasets.py index ada7de0ce20..b87cdc3ecaa 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_datasets.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_datasets.py @@ -8,6 +8,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock, call, patch import pytest from flask import Flask +from sqlalchemy.orm import Session from werkzeug.exceptions import BadRequest, Forbidden, NotFound import services @@ -44,7 +45,7 @@ from core.rag.index_processor.constant.index_type import IndexStructureType from core.rag.retrieval.retrieval_methods import RetrievalMethod from extensions.storage.storage_type import StorageType from models.account import Account, TenantAccountRole -from models.dataset import Dataset, DatasetQuery, Document +from models.dataset import AppDatasetJoin, Dataset, DatasetPermission, DatasetQuery, Document, DocumentSegment from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom, IndexingStatus from models.model import ApiToken, App, AppMode, IconType, UploadFile from services.dataset_ref_service import DatasetRef @@ -170,7 +171,29 @@ def make_document_status(**overrides) -> Document: return Document(**base) -class TestDatasetList: +def make_document_segment(*, position: int, completed: bool) -> DocumentSegment: + return DocumentSegment( + tenant_id="tenant-1", + dataset_id="dataset-1", + document_id="doc-1", + position=position, + content=f"segment {position}", + word_count=2, + tokens=2, + created_by="account-1", + completed_at=datetime.datetime(2024, 1, 1, tzinfo=datetime.UTC) if completed else None, + ) + + +class _UsesSQLiteSession: + session: Session + + @pytest.fixture(autouse=True) + def _inject_sqlite_session(self, sqlite_session: Session) -> None: + self.session = sqlite_session + + +class TestDatasetList(_UsesSQLiteSession): def _mock_user(self): user = make_account() return user @@ -185,7 +208,7 @@ class TestDatasetList: patch.object(DatasetService, "get_datasets", return_value=(datasets, 1)), patch.object(ProviderManager, "get_configurations", return_value=MagicMock(get_models=lambda **_: [])), ): - resp, status = method(api, MagicMock(), "tenant-1", current_user) + resp, status = method(api, self.session, "tenant-1", current_user) assert status == 200 assert resp["total"] == 1 assert resp["data"][0]["embedding_available"] is True @@ -201,7 +224,7 @@ class TestDatasetList: method = unwrap(api.get) current_user = self._mock_user() dataset = make_dataset() - session = MagicMock() + session = self.session with app.test_request_context("/datasets"): with ( patch.object(DatasetService, "get_datasets", return_value=([dataset], 1)), @@ -222,7 +245,7 @@ class TestDatasetList: patch.object(DatasetService, "get_datasets_by_ids", return_value=(datasets, 2)) as by_ids_mock, patch.object(ProviderManager, "get_configurations", return_value=MagicMock(get_models=lambda **_: [])), ): - resp, status = method(api, MagicMock(), "tenant-1", current_user) + resp, status = method(api, self.session, "tenant-1", current_user) by_ids_mock.assert_called_once() assert status == 200 assert resp["total"] == 2 @@ -251,7 +274,7 @@ class TestDatasetList: return_value=permissions, ) as get_permissions, ): - resp, status = method(api, MagicMock(), "tenant-1", current_user) + resp, status = method(api, self.session, "tenant-1", current_user) get_permissions.assert_called_once_with("tenant-1", current_user.id, session=ANY) assert status == 200 assert resp["data"][0]["permission_keys"] == ["dataset.acl.readonly", "dataset.acl.edit"] @@ -281,7 +304,7 @@ class TestDatasetList: ), patch.object(ProviderManager, "get_configurations", return_value=MagicMock(get_models=lambda **_: [])), ): - method(api, MagicMock(), "tenant-1", current_user) + method(api, self.session, "tenant-1", current_user) assert get_datasets.call_args.kwargs["accessible_dataset_ids"] == [] assert get_datasets.call_args.kwargs["include_own_datasets"] is False @@ -308,7 +331,7 @@ class TestDatasetList: ), patch.object(ProviderManager, "get_configurations", return_value=MagicMock(get_models=lambda **_: [])), ): - method(api, MagicMock(), "tenant-1", current_user) + method(api, self.session, "tenant-1", current_user) assert get_datasets.call_args.kwargs["accessible_dataset_ids"] is None def test_get_restricted_whitelist_overrides_default_read_permission( @@ -374,7 +397,7 @@ class TestDatasetList: ), patch.object(ProviderManager, "get_configurations", return_value=MagicMock(get_models=lambda **_: [])), ): - method(api, MagicMock(), "tenant-1", current_user) + method(api, self.session, "tenant-1", current_user) assert get_datasets.call_args.kwargs["accessible_dataset_ids"] == [ "dataset-whitelist-only", ] @@ -399,9 +422,9 @@ class TestDatasetList: ), patch.object(ProviderManager, "get_configurations", return_value=MagicMock(get_models=lambda **_: [])), ): - method(api, MagicMock(), "tenant-1", current_user) + method(api, self.session, "tenant-1", current_user) session = get_datasets_by_ids.call_args.kwargs["session"] - assert isinstance(session, MagicMock) + assert session is self.session assert get_datasets_by_ids.call_args.args == (["dataset-1"], "tenant-1") assert get_datasets_by_ids.call_args.kwargs == { "user": current_user, @@ -420,7 +443,7 @@ class TestDatasetList: patch.object(DatasetService, "get_datasets", return_value=(datasets, 1)), patch.object(ProviderManager, "get_configurations", return_value=MagicMock(get_models=lambda **_: [])), ): - resp, status = method(api, MagicMock(), "tenant-1", current_user) + resp, status = method(api, self.session, "tenant-1", current_user) assert status == 200 def test_get_allows_legacy_weighted_score_without_weight_type(self, app: Flask): @@ -453,7 +476,7 @@ class TestDatasetList: patch.object(DatasetService, "get_datasets", return_value=(datasets, 1)), patch.object(ProviderManager, "get_configurations", return_value=MagicMock(get_models=lambda **_: [])), ): - resp, status = method(api, MagicMock(), "tenant-1", current_user) + resp, status = method(api, self.session, "tenant-1", current_user) assert status == 200 assert resp["data"][0]["retrieval_model_dict"]["weights"]["weight_type"] is None @@ -467,7 +490,7 @@ class TestDatasetList: patch.object(DatasetService, "get_datasets", return_value=(datasets, 1)), patch.object(ProviderManager, "get_configurations", return_value=MagicMock(get_models=lambda **_: [])), ): - resp, status = method(api, MagicMock(), "tenant-1", current_user) + resp, status = method(api, self.session, "tenant-1", current_user) assert status == 200 retrieval_model = resp["data"][0]["retrieval_model_dict"] assert retrieval_model["search_method"] == "semantic_search" @@ -491,7 +514,7 @@ class TestDatasetList: patch.object(DatasetService, "get_datasets", return_value=(datasets, 1)), patch.object(ProviderManager, "get_configurations", return_value=config), ): - resp, status = method(api, MagicMock(), "tenant-1", current_user) + resp, status = method(api, self.session, "tenant-1", current_user) assert resp["data"][0]["embedding_available"] is False def test_partial_members_permission(self, app: Flask): @@ -499,8 +522,9 @@ class TestDatasetList: method = unwrap(api.get) current_user = self._mock_user() datasets = [make_dataset(permission="partial_members")] - session = MagicMock() - session.execute.return_value.all.return_value = [("ds-1", "u1")] + session = self.session + session.add(DatasetPermission(dataset_id="ds-1", account_id="u1", tenant_id="tenant-1")) + session.flush() with app.test_request_context("/datasets"): with ( patch.object(DatasetService, "get_datasets", return_value=(datasets, 1)), @@ -510,7 +534,7 @@ class TestDatasetList: assert resp["data"][0]["partial_member_list"] == ["u1"] -class TestDatasetListApiPost: +class TestDatasetListApiPost(_UsesSQLiteSession): def test_post_success(self, app: Flask): api = DatasetListApi() method = unwrap(api.post) @@ -522,7 +546,7 @@ class TestDatasetListApiPost: patch.object(type(console_ns), "payload", payload), patch.object(DatasetService, "create_empty_dataset", return_value=dataset), ): - _, status = method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", user) + _, status = method(api, DatasetCreatePayload(**payload), self.session, "tenant-1", user) assert status == 201 def test_post_forbidden(self, app: Flask): @@ -532,7 +556,7 @@ class TestDatasetListApiPost: user = make_account(TenantAccountRole.NORMAL) with app.test_request_context("/datasets", json=payload), patch.object(type(console_ns), "payload", payload): with pytest.raises(Forbidden): - method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", user) + method(api, DatasetCreatePayload(**payload), self.session, "tenant-1", user) def test_post_duplicate_name(self, app: Flask): api = DatasetListApi() @@ -547,14 +571,14 @@ class TestDatasetListApiPost: ), ): with pytest.raises(DatasetNameDuplicateError): - method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", user) + method(api, DatasetCreatePayload(**payload), self.session, "tenant-1", user) def test_post_invalid_payload_missing_name(self, app: Flask): api = DatasetListApi() method = unwrap(api.post) with app.test_request_context("/datasets", json={}), patch.object(type(console_ns), "payload", {}): with pytest.raises(ValueError): - method(api, DatasetCreatePayload(), MagicMock(), "tenant-1", make_account()) + method(api, DatasetCreatePayload(), self.session, "tenant-1", make_account()) def test_post_invalid_indexing_technique(self, app: Flask): api = DatasetListApi() @@ -562,7 +586,7 @@ class TestDatasetListApiPost: payload = {"name": "bad", "indexing_technique": "invalid-tech"} with app.test_request_context("/datasets", json=payload), patch.object(type(console_ns), "payload", payload): with pytest.raises(ValueError, match="Invalid indexing technique"): - method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", make_account()) + method(api, DatasetCreatePayload(**payload), self.session, "tenant-1", make_account()) def test_post_invalid_provider(self, app: Flask): api = DatasetListApi() @@ -570,10 +594,10 @@ class TestDatasetListApiPost: payload = {"name": "bad", "provider": "unknown"} with app.test_request_context("/datasets", json=payload), patch.object(type(console_ns), "payload", payload): with pytest.raises(ValueError, match="Invalid provider"): - method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", make_account()) + method(api, DatasetCreatePayload(**payload), self.session, "tenant-1", make_account()) -class TestDatasetApiGet: +class TestDatasetApiGet(_UsesSQLiteSession): def test_get_success_basic(self, app: Flask): api = DatasetApi() method = unwrap(api.get) @@ -588,7 +612,7 @@ class TestDatasetApiGet: patch("controllers.console.datasets.datasets.create_plugin_provider_manager") as provider_manager_mock, ): provider_manager_mock.return_value.get_configurations.return_value.get_models.return_value = [] - data, status = method(api, MagicMock(), tenant_id, user, dataset_id) + data, status = method(api, self.session, tenant_id, user, dataset_id) assert status == 200 assert data["embedding_available"] is True @@ -597,7 +621,7 @@ class TestDatasetApiGet: api = DatasetApi() method = unwrap(api.get) dataset_id = "123e4567-e89b-12d3-a456-426614174000" - user = MagicMock(id="account-1") + user = make_account() tenant_id = "tenant-1" dataset = make_dataset(id=dataset_id) with ( @@ -619,7 +643,7 @@ class TestDatasetApiGet: patch("controllers.console.datasets.datasets.create_plugin_provider_manager") as provider_manager_mock, ): provider_manager_mock.return_value.get_configurations.return_value.get_models.return_value = [] - data, status = method(api, MagicMock(), tenant_id, user, dataset_id) + data, status = method(api, self.session, tenant_id, user, dataset_id) get_permissions.assert_called_once_with(tenant_id, user.id, dataset_id=dataset_id, session=ANY) assert status == 200 assert data["permission_keys"] == ["dataset.acl.readonly", "dataset.acl.edit"] @@ -636,7 +660,7 @@ class TestDatasetApiGet: patch("controllers.console.datasets.datasets.create_plugin_provider_manager") as provider_manager_mock, ): provider_manager_mock.return_value.get_configurations.return_value.get_models.return_value = [] - data, status = method(api, MagicMock(), "tenant", make_account(), dataset_id) + data, status = method(api, self.session, "tenant", make_account(), dataset_id) assert status == 200 assert data["external_retrieval_model"] == {"top_k": 2, "score_threshold": 0.0, "score_threshold_enabled": None} @@ -649,7 +673,7 @@ class TestDatasetApiGet: patch.object(DatasetService, "get_dataset", return_value=None), ): with pytest.raises(NotFound, match="Dataset not found"): - method(api, MagicMock(), "tenant", make_account(), dataset_id) + method(api, self.session, "tenant", make_account(), dataset_id) def test_get_permission_denied(self, app: Flask): api = DatasetApi() @@ -666,7 +690,7 @@ class TestDatasetApiGet: ), ): with pytest.raises(Forbidden, match="no access"): - method(api, MagicMock(), "tenant", make_account(), dataset_id) + method(api, self.session, "tenant", make_account(), dataset_id) def test_get_high_quality_embedding_unavailable(self, app: Flask): api = DatasetApi() @@ -687,7 +711,7 @@ class TestDatasetApiGet: patch("controllers.console.datasets.datasets.create_plugin_provider_manager") as provider_manager_mock, ): provider_manager_mock.return_value.get_configurations.return_value.get_models.return_value = [] - data, _ = method(api, MagicMock(), tenant_id, user, dataset_id) + data, _ = method(api, self.session, tenant_id, user, dataset_id) assert data["embedding_available"] is False def test_get_partial_members_permission(self, app: Flask): @@ -704,11 +728,11 @@ class TestDatasetApiGet: patch("controllers.console.datasets.datasets.create_plugin_provider_manager") as provider_manager_mock, ): provider_manager_mock.return_value.get_configurations.return_value.get_models.return_value = [] - data, _ = method(api, MagicMock(), "tenant", make_account(), dataset_id) + data, _ = method(api, self.session, "tenant", make_account(), dataset_id) assert data["partial_member_list"] == partial_members -class TestDatasetApiPatch: +class TestDatasetApiPatch(_UsesSQLiteSession): def test_patch_success_basic(self, app: Flask): api = DatasetApi() method = unwrap(api.patch) @@ -725,7 +749,7 @@ class TestDatasetApiPatch: patch.object(DatasetService, "update_dataset", return_value=dataset), patch.object(DatasetPermissionService, "get_dataset_partial_member_list", return_value=[]), ): - result, status = method(api, DatasetUpdatePayload(), MagicMock(), tenant_id, user, dataset_id) + result, status = method(api, DatasetUpdatePayload(), self.session, tenant_id, user, dataset_id) assert status == 200 assert result["partial_member_list"] == [] @@ -737,7 +761,7 @@ class TestDatasetApiPatch: patch.object(DatasetService, "get_dataset", return_value=None), ): with pytest.raises(NotFound, match="Dataset not found"): - method(api, DatasetUpdatePayload(), MagicMock(), "tenant-1", make_account(), "missing") + method(api, DatasetUpdatePayload(), self.session, "tenant-1", make_account(), "missing") def test_patch_permission_denied(self, app: Flask): api = DatasetApi() @@ -752,7 +776,7 @@ class TestDatasetApiPatch: patch.object(DatasetPermissionService, "check_permission", side_effect=Forbidden("no permission")), ): with pytest.raises(Forbidden): - method(api, DatasetUpdatePayload(), MagicMock(), "tenant", make_account(), dataset_id) + method(api, DatasetUpdatePayload(), self.session, "tenant", make_account(), dataset_id) def test_patch_partial_members_update(self, app: Flask): api = DatasetApi() @@ -769,7 +793,7 @@ class TestDatasetApiPatch: patch.object(DatasetPermissionService, "update_partial_member_list", return_value=None), patch.object(DatasetPermissionService, "get_dataset_partial_member_list", return_value=["u1", "u2"]), ): - result, _ = method(api, DatasetUpdatePayload(), MagicMock(), "tenant", make_account(), dataset_id) + result, _ = method(api, DatasetUpdatePayload(), self.session, "tenant", make_account(), dataset_id) assert result["partial_member_list"] == ["u1", "u2"] def test_patch_clear_partial_members(self, app: Flask): @@ -787,11 +811,11 @@ class TestDatasetApiPatch: patch.object(DatasetPermissionService, "clear_partial_member_list", return_value=None), patch.object(DatasetPermissionService, "get_dataset_partial_member_list", return_value=[]), ): - result, _ = method(api, DatasetUpdatePayload(), MagicMock(), "tenant", make_account(), dataset_id) + result, _ = method(api, DatasetUpdatePayload(), self.session, "tenant", make_account(), dataset_id) assert result["partial_member_list"] == [] -class TestDatasetApiDelete: +class TestDatasetApiDelete(_UsesSQLiteSession): def test_delete_success(self, app: Flask): api = DatasetApi() method = unwrap(api.delete) @@ -802,7 +826,7 @@ class TestDatasetApiDelete: patch.object(DatasetService, "delete_dataset", return_value=True), patch.object(DatasetPermissionService, "clear_partial_member_list", return_value=None), ): - result, status = method(api, MagicMock(), user, dataset_id) + result, status = method(api, self.session, user, dataset_id) assert status == 204 assert result == "" @@ -813,7 +837,7 @@ class TestDatasetApiDelete: user = make_account(TenantAccountRole.NORMAL) with app.test_request_context(f"/datasets/{dataset_id}"): with pytest.raises(Forbidden): - method(api, MagicMock(), user, dataset_id) + method(api, self.session, user, dataset_id) def test_delete_dataset_not_found(self, app: Flask): api = DatasetApi() @@ -825,7 +849,7 @@ class TestDatasetApiDelete: patch.object(DatasetService, "delete_dataset", return_value=False), ): with pytest.raises(NotFound, match="Dataset not found"): - method(api, MagicMock(), user, dataset_id) + method(api, self.session, user, dataset_id) def test_delete_dataset_in_use(self, app: Flask): api = DatasetApi() @@ -837,10 +861,10 @@ class TestDatasetApiDelete: patch.object(DatasetService, "delete_dataset", side_effect=services.errors.dataset.DatasetInUseError()), ): with pytest.raises(DatasetInUseError): - method(api, MagicMock(), user, dataset_id) + method(api, self.session, user, dataset_id) -class TestDatasetUseCheckApi: +class TestDatasetUseCheckApi(_UsesSQLiteSession): @pytest.mark.parametrize("is_using", [True, False]) def test_get_use_check(self, app: Flask, is_using: bool): api = DatasetUseCheckApi() @@ -848,7 +872,7 @@ class TestDatasetUseCheckApi: dataset_id = "dataset-id" dataset = make_dataset(id=dataset_id) current_user = make_account() - session = MagicMock() + session = self.session with ( app.test_request_context(f"/datasets/{dataset_id}/use-check"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset) as get_dataset, @@ -867,7 +891,7 @@ class TestDatasetUseCheckApi: api = DatasetUseCheckApi() method = unwrap(api.get) dataset = make_dataset(id="dataset-id") - session = MagicMock() + session = self.session with ( app.test_request_context("/datasets/dataset-id/use-check"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset), @@ -884,11 +908,11 @@ class TestDatasetUseCheckApi: "api_cls", [DatasetUseCheckApi, DatasetIndexingStatusApi, DatasetErrorDocs, DatasetAutoDisableLogApi], ) -def test_dataset_scoped_read_permission_denied(app: Flask, api_cls): +def test_dataset_scoped_read_permission_denied(app: Flask, api_cls, sqlite_session: Session): api = api_cls() method = unwrap(api.get) dataset = make_dataset(id="dataset-1") - session = MagicMock() + session = sqlite_session with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset), @@ -902,7 +926,7 @@ def test_dataset_scoped_read_permission_denied(app: Flask, api_cls): method(api, session, "tenant-1", make_account(), "dataset-1") -class TestDatasetQueryApi: +class TestDatasetQueryApi(_UsesSQLiteSession): def _query_record(self, index: int = 1) -> DatasetQuery: query = DatasetQuery( dataset_id="dataset-id", @@ -929,7 +953,7 @@ class TestDatasetQueryApi: patch.object(DatasetService, "check_dataset_permission", return_value=None), patch.object(DatasetService, "get_dataset_queries", return_value=(queries, 2)), ): - response, status = method(api, MagicMock(), current_user, dataset_id) + response, status = method(api, self.session, current_user, dataset_id) assert status == 200 assert response["total"] == 2 assert response["page"] == 1 @@ -952,24 +976,30 @@ class TestDatasetQueryApi: dataset = make_dataset(id="dataset-id") query = self._query_record() query.content = json.dumps([{"content_type": "image_query", "content": "file-1"}]) - upload_file = SimpleNamespace( - id="file-1", + upload_file = UploadFile( + tenant_id="tenant-1", + storage_type=StorageType.LOCAL, + key="image.png", name="image.png", size=10, extension="png", mime_type="image/png", + created_by_role=CreatorUserRole.ACCOUNT, + created_by="account-1", + created_at=datetime.datetime(2024, 1, 1, tzinfo=datetime.UTC), + used=False, ) - session = MagicMock() - session.scalar.return_value = upload_file + upload_file.id = "file-1" + session = self.session + session.add(upload_file) + session.flush() with ( app.test_request_context("/datasets/queries"), patch.object(DatasetService, "get_dataset", return_value=dataset), patch.object(DatasetService, "check_dataset_permission", return_value=None), patch.object(DatasetService, "get_dataset_queries", return_value=([query], 1)), - patch("models.dataset.db") as db_mock, patch("models.dataset.sign_upload_file_preview_url", return_value="signed-url"), ): - db_mock.session.scalar.return_value = upload_file response, status = method(api, session, make_account(), "dataset-id") assert status == 200 @@ -987,8 +1017,7 @@ class TestDatasetQueryApi: }, } ] - session.scalar.assert_called_once() - db_mock.session.scalar.assert_not_called() + assert session.get(UploadFile, "file-1") is upload_file def test_get_queries_dataset_not_found(self, app: Flask): api = DatasetQueryApi() @@ -1000,7 +1029,7 @@ class TestDatasetQueryApi: patch.object(DatasetService, "get_dataset", return_value=None), ): with pytest.raises(NotFound, match="Dataset not found"): - method(api, MagicMock(), current_user, dataset_id) + method(api, self.session, current_user, dataset_id) def test_get_queries_permission_denied(self, app: Flask): api = DatasetQueryApi() @@ -1018,7 +1047,7 @@ class TestDatasetQueryApi: ), ): with pytest.raises(Forbidden): - method(api, MagicMock(), current_user, dataset_id) + method(api, self.session, current_user, dataset_id) def test_get_queries_pagination_has_more(self, app: Flask): api = DatasetQueryApi() @@ -1033,13 +1062,13 @@ class TestDatasetQueryApi: patch.object(DatasetService, "check_dataset_permission", return_value=None), patch.object(DatasetService, "get_dataset_queries", return_value=(queries, 40)), ): - response, status = method(api, MagicMock(), current_user, dataset_id) + response, status = method(api, self.session, current_user, dataset_id) assert status == 200 assert response["has_more"] is True assert len(response["data"]) == 20 -class TestDatasetIndexingEstimateApi: +class TestDatasetIndexingEstimateApi(_UsesSQLiteSession): def _upload_file(self, *, tenant_id: str = "tenant-1", file_id: str = "file-1") -> UploadFile: upload_file = UploadFile( tenant_id=tenant_id, @@ -1072,8 +1101,9 @@ class TestDatasetIndexingEstimateApi: method = unwrap(api.post) payload = self._base_payload() mock_file = self._upload_file() - session = MagicMock() - session.scalars.return_value.all.return_value = [mock_file] + session = self.session + session.add(mock_file) + session.flush() mock_response = IndexingEstimate(total_segments=100, preview=[]) @@ -1102,8 +1132,7 @@ class TestDatasetIndexingEstimateApi: api = DatasetIndexingEstimateApi() method = unwrap(api.post) payload = self._base_payload() - session = MagicMock() - session.scalars.return_value.all.return_value = None + session = self.session with ( app.test_request_context("/"), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), @@ -1122,8 +1151,9 @@ class TestDatasetIndexingEstimateApi: method = unwrap(api.post) mock_file = self._upload_file() payload = self._base_payload() - session = MagicMock() - session.scalars.return_value.all.return_value = [mock_file] + session = self.session + session.add(mock_file) + session.flush() with ( app.test_request_context("/"), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), @@ -1146,8 +1176,9 @@ class TestDatasetIndexingEstimateApi: method = unwrap(api.post) mock_file = self._upload_file() payload = self._base_payload() - session = MagicMock() - session.scalars.return_value.all.return_value = [mock_file] + session = self.session + session.add(mock_file) + session.flush() with ( app.test_request_context("/"), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), @@ -1170,8 +1201,9 @@ class TestDatasetIndexingEstimateApi: method = unwrap(api.post) mock_file = self._upload_file() payload = self._base_payload() - session = MagicMock() - session.scalars.return_value.all.return_value = [mock_file] + session = self.session + session.add(mock_file) + session.flush() with ( app.test_request_context("/"), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), @@ -1189,16 +1221,16 @@ class TestDatasetIndexingEstimateApi: ) -class TestDatasetRelatedAppListApi: +class TestDatasetRelatedAppListApi(_UsesSQLiteSession): def test_get_success(self, app: Flask): api = DatasetRelatedAppListApi() method = unwrap(api.get) dataset = make_dataset(id="dataset-1") app1 = make_related_app(id="app-1", name="App 1") app2 = make_related_app(id="app-2", name="App 2") - join1 = MagicMock(app_id="app-1") - join2 = MagicMock(app_id="app-2") - session = MagicMock() + join1 = AppDatasetJoin(app_id="app-1", dataset_id="dataset-1") + join2 = AppDatasetJoin(app_id="app-2", dataset_id="dataset-1") + session = self.session with ( app.test_request_context("/"), patch("controllers.console.datasets.datasets.DatasetService.get_dataset", return_value=dataset), @@ -1251,7 +1283,7 @@ class TestDatasetRelatedAppListApi: patch("controllers.console.datasets.datasets.DatasetService.get_dataset", return_value=None), ): with pytest.raises(NotFound): - method(api, MagicMock(), make_account(), "dataset-1") + method(api, self.session, make_account(), "dataset-1") def test_get_permission_denied(self, app: Flask): api = DatasetRelatedAppListApi() @@ -1266,16 +1298,16 @@ class TestDatasetRelatedAppListApi: ), ): with pytest.raises(Forbidden): - method(api, MagicMock(), make_account(), "dataset-1") + method(api, self.session, make_account(), "dataset-1") def test_get_filters_none_apps(self, app: Flask): api = DatasetRelatedAppListApi() method = unwrap(api.get) dataset = make_dataset(id="dataset-1") app1 = make_related_app() - join1 = MagicMock(app_id="app-1") - join2 = MagicMock(app_id="app-2") - session = MagicMock() + join1 = AppDatasetJoin(app_id="app-1", dataset_id="dataset-1") + join2 = AppDatasetJoin(app_id="app-2", dataset_id="dataset-1") + session = self.session with ( app.test_request_context("/"), patch("controllers.console.datasets.datasets.DatasetService.get_dataset", return_value=dataset), @@ -1303,26 +1335,17 @@ class TestDatasetRelatedAppListApi: ] -class TestDatasetIndexingStatusApi: +class TestDatasetIndexingStatusApi(_UsesSQLiteSession): def test_get_success_with_documents(self, app: Flask): api = DatasetIndexingStatusApi() method = unwrap(api.get) dataset = make_dataset(id="dataset-1") current_user = make_account() - document = MagicMock() - document.id = "doc-1" - document.indexing_status = "completed" - document.processing_started_at = None - document.parsing_completed_at = None - document.cleaning_completed_at = None - document.splitting_completed_at = None - document.completed_at = None - document.paused_at = None - document.error = None - document.stopped_at = None - session = MagicMock() - session.scalars.return_value.all.return_value = [document] - session.scalar.return_value = 3 + document = make_document_status() + session = self.session + session.add(document) + session.add_all([make_document_segment(position=position, completed=True) for position in range(1, 4)]) + session.flush() with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset) as get_dataset, @@ -1337,16 +1360,13 @@ class TestDatasetIndexingStatusApi: assert item["total_segments"] == 3 get_dataset.assert_called_once_with("dataset-1", "tenant-1", session=session) check_permission.assert_called_once_with(dataset, current_user, session) - assert {"dataset-1", "tenant-1"} <= set(session.scalars.call_args.args[0].compile().params.values()) - for segment_count_call in session.scalar.call_args_list: - assert {"dataset-1", "tenant-1", "doc-1"} <= set(segment_count_call.args[0].compile().params.values()) + assert session.get(Document, "doc-1") is document def test_get_success_no_documents(self, app: Flask): api = DatasetIndexingStatusApi() method = unwrap(api.get) dataset = make_dataset(id="dataset-1") - session = MagicMock() - session.scalars.return_value.all.return_value = [] + session = self.session with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset), @@ -1360,20 +1380,11 @@ class TestDatasetIndexingStatusApi: api = DatasetIndexingStatusApi() method = unwrap(api.get) dataset = make_dataset(id="dataset-1") - document = MagicMock() - document.id = "doc-1" - document.indexing_status = "indexing" - document.processing_started_at = None - document.parsing_completed_at = None - document.cleaning_completed_at = None - document.splitting_completed_at = None - document.completed_at = None - document.paused_at = None - document.error = None - document.stopped_at = None - session = MagicMock() - session.scalars.return_value.all.return_value = [document] - session.scalar.side_effect = [2, 5] + document = make_document_status(indexing_status=IndexingStatus.INDEXING) + session = self.session + session.add(document) + session.add_all([make_document_segment(position=position, completed=position <= 2) for position in range(1, 6)]) + session.flush() with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset), @@ -1386,7 +1397,7 @@ class TestDatasetIndexingStatusApi: assert item["total_segments"] == 5 -class TestDatasetApiKeyApi: +class TestDatasetApiKeyApi(_UsesSQLiteSession): def test_get_api_keys_success(self, app: Flask): api = DatasetApiKeyApi() method = unwrap(api.get) @@ -1404,8 +1415,11 @@ class TestDatasetApiKeyApi: last_used_at=None, created_at=None, ) - session = MagicMock() - session.scalars.return_value.all.return_value = [mock_key_1, mock_key_2] + session = self.session + mock_key_1.tenant_id = "tenant-1" + mock_key_2.tenant_id = "tenant-1" + session.add_all([mock_key_1, mock_key_2]) + session.flush() with app.test_request_context("/"): response = method(api, session, "tenant-1") assert "data" in response @@ -1418,30 +1432,31 @@ class TestDatasetApiKeyApi: def test_post_create_api_key_success(self, app: Flask): api = DatasetApiKeyApi() method = unwrap(api.post) - mock_token = MagicMock() - mock_token.id = "new-key-id" - mock_token.last_used_at = None - mock_token.created_at = datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.UTC) - mock_api_token_cls = MagicMock() - mock_api_token_cls.return_value = mock_token - mock_api_token_cls.generate_api_key.return_value = "dataset-abc123" - session = MagicMock() - session.scalar.return_value = 3 - with app.test_request_context("/"), patch("controllers.console.datasets.datasets.ApiToken", mock_api_token_cls): + session = self.session + with ( + app.test_request_context("/"), + patch.object(ApiToken, "generate_api_key", return_value="dataset-abc123") as generate_api_key, + ): response, status = method(api, session, "tenant-1") assert status == 200 assert isinstance(response, dict) - assert response["id"] == "new-key-id" assert response["token"] == "dataset-abc123" assert response["type"] == "dataset" assert response["created_at"] is not None - mock_api_token_cls.generate_api_key.assert_called_once_with("dataset-", 24, session=session) + generate_api_key.assert_called_once_with("dataset-", 24, session=session) + assert session.get(ApiToken, response["id"]).token == "dataset-abc123" def test_post_exceed_max_keys(self, app: Flask): api = DatasetApiKeyApi() method = unwrap(api.post) - session = MagicMock() - session.scalar.return_value = 10 + session = self.session + session.add_all( + [ + ApiToken(id=f"key-{index}", tenant_id="tenant-1", type="dataset", token=f"ds-{index}") + for index in range(10) + ] + ) + session.flush() with app.test_request_context("/"): with pytest.raises(BadRequest) as exc_info: method(api, session, "tenant-1") @@ -1452,36 +1467,42 @@ class TestDatasetApiKeyApi: } -class TestDatasetApiDeleteApi: +class TestDatasetApiDeleteApi(_UsesSQLiteSession): def test_delete_success(self, app: Flask): api = DatasetApiDeleteApi() method = unwrap(api.delete) - mock_key = MagicMock() - session = MagicMock() - session.scalar.return_value = mock_key - with app.test_request_context("/"): + session = self.session + key = ApiToken(id="api-key-id", tenant_id="tenant-1", type="dataset", token="dataset-secret") + session.add(key) + session.flush() + with ( + app.test_request_context("/"), + patch("controllers.console.datasets.datasets.ApiTokenCache.delete") as delete_cache, + ): response, status = method(api, session, "tenant-1", "api-key-id") assert status == 204 assert response == "" + delete_cache.assert_called_once() + session.flush() + assert session.get(ApiToken, "api-key-id") is None def test_delete_key_not_found(self, app: Flask): api = DatasetApiDeleteApi() method = unwrap(api.delete) - session = MagicMock() - session.scalar.return_value = None + session = self.session with app.test_request_context("/"): with pytest.raises(NotFound): method(api, session, "tenant-1", "api-key-id") -class TestDatasetEnableApiApi: +class TestDatasetEnableApiApi(_UsesSQLiteSession): @pytest.mark.parametrize(("status_value", "enabled"), [("enable", True), ("disable", False)]) def test_update_api_status(self, app: Flask, status_value: str, enabled: bool): api = DatasetEnableApiApi() method = unwrap(api.post) dataset = make_dataset(id="dataset-1") current_user = make_account() - session = MagicMock() + session = self.session with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset) as get_dataset, @@ -1499,7 +1520,7 @@ class TestDatasetEnableApiApi: api = DatasetEnableApiApi() method = unwrap(api.post) dataset = make_dataset(id="dataset-1") - session = MagicMock() + session = self.session with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset), @@ -1582,7 +1603,7 @@ class TestDatasetRetrievalSettingApi: ] -class TestDatasetRetrievalSettingMockApi: +class TestDatasetRetrievalSettingMockApi(_UsesSQLiteSession): def test_get_success(self, app: Flask): api = DatasetRetrievalSettingMockApi() method = unwrap(api.get) @@ -1597,14 +1618,14 @@ class TestDatasetRetrievalSettingMockApi: assert response["retrieval_method"] == ["semantic"] -class TestDatasetErrorDocs: +class TestDatasetErrorDocs(_UsesSQLiteSession): def test_get_success(self, app: Flask): api = DatasetErrorDocs() method = unwrap(api.get) dataset = make_dataset(id="dataset-1") error_doc = make_document_status(id="error-doc", indexing_status=IndexingStatus.ERROR, error="failed") current_user = make_account() - session = MagicMock() + session = self.session with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset) as get_dataset, @@ -1624,7 +1645,7 @@ class TestDatasetErrorDocs: def test_get_dataset_not_found(self, app: Flask): api = DatasetErrorDocs() method = unwrap(api.get) - session = MagicMock() + session = self.session with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=None) as get_dataset, @@ -1634,7 +1655,7 @@ class TestDatasetErrorDocs: get_dataset.assert_called_once_with("dataset-1", "tenant-1", session=session) -class TestDatasetPermissionUserListApi: +class TestDatasetPermissionUserListApi(_UsesSQLiteSession): def test_get_success(self, app: Flask): api = DatasetPermissionUserListApi() method = unwrap(api.get) @@ -1649,7 +1670,7 @@ class TestDatasetPermissionUserListApi: return_value=users, ), ): - response, status = method(api, MagicMock(), make_account(), "dataset-1") + response, status = method(api, self.session, make_account(), "dataset-1") assert status == 200 assert response["data"] == users @@ -1666,17 +1687,17 @@ class TestDatasetPermissionUserListApi: ), ): with pytest.raises(Forbidden): - method(api, MagicMock(), make_account(), "dataset-1") + method(api, self.session, make_account(), "dataset-1") -class TestDatasetAutoDisableLogApi: +class TestDatasetAutoDisableLogApi(_UsesSQLiteSession): def test_get_success(self, app: Flask): api = DatasetAutoDisableLogApi() method = unwrap(api.get) dataset = make_dataset(id="dataset-1") logs = {"document_ids": ["doc-1"], "count": 1} current_user = make_account() - session = MagicMock() + session = self.session with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=dataset) as get_dataset, @@ -1693,7 +1714,7 @@ class TestDatasetAutoDisableLogApi: def test_get_dataset_not_found(self, app: Flask): api = DatasetAutoDisableLogApi() method = unwrap(api.get) - session = MagicMock() + session = self.session with ( app.test_request_context("/"), patch.object(DatasetService, "get_dataset_for_tenant", return_value=None) as get_dataset, diff --git a/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py b/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py index a4f14e5255d..2af0ce3db7a 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py @@ -1,11 +1,13 @@ +from collections.abc import Callable from inspect import unwrap +from types import SimpleNamespace from unittest.mock import patch import pytest -from flask import Flask, g +from flask import Flask, g, request from pydantic_core import ValidationError from sqlalchemy.orm import Session -from werkzeug.exceptions import Forbidden +from werkzeug.exceptions import Forbidden, UnprocessableEntity from configs import dify_config from controllers.console.workspace.model_providers import ( @@ -17,6 +19,14 @@ from controllers.console.workspace.model_providers import ( ModelProviderPaymentCheckoutUrlApi, ModelProviderSummaryListApi, ModelProviderValidateApi, + ParserCredentialCreate, + ParserCredentialDelete, + ParserCredentialId, + ParserCredentialSwitch, + ParserCredentialUpdate, + ParserCredentialValidate, + ParserModelList, + ParserPreferredProviderType, PreferredProviderTypeUpdateApi, ) from core.entities.provider_entities import CredentialConfiguration @@ -26,6 +36,7 @@ from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.entities.provider_entities import ConfigurateMethod from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from models import Account +from models.account import TenantAccountRole from models.provider import ProviderType from services.entities.model_provider_entities import ( CustomConfigurationResponse, @@ -116,6 +127,19 @@ def expected_provider_payload() -> dict[str, object]: } +def _payload() -> dict[str, object]: + """Mirror the source the ``@model_validate`` decorator reads for the request in scope. + + The tests build their contexts with ``test_request_context``, which defaults to GET even for + handlers mounted on POST, so fall back to the JSON body whenever the query string is empty. + """ + args: dict[str, object] = dict(request.args.to_dict(flat=True)) + if args: + return args + body = request.get_json(silent=True) + return dict(body) if isinstance(body, dict) else {} + + class TestModelProviderListApi: def test_get_success(self, app: Flask): api = ModelProviderListApi() @@ -129,7 +153,7 @@ class TestModelProviderListApi: return_value=[provider], ) as get_provider_list, ): - result = method(api, "tenant1") + result = method(api, ParserModelList.model_validate(_payload()), "tenant1") get_provider_list.assert_called_once_with(tenant_id="tenant1", model_type=ModelType.LLM) assert result == {"data": [expected_provider_payload()]} @@ -145,7 +169,7 @@ class TestModelProviderListApi: return_value=[], ) as get_provider_list, ): - result = method(api, "tenant1") + result = method(api, ParserModelList.model_validate(_payload()), "tenant1") get_provider_list.assert_called_once_with(tenant_id="tenant1", model_type=None) assert result == {"data": []} @@ -297,7 +321,7 @@ class TestModelProviderCredentialApi: }, ) as get_provider_credential, ): - result = method(api, "tenant1", provider="openai") + result = method(api, ParserCredentialId.model_validate(_payload()), "tenant1", provider="openai") get_provider_credential.assert_called_once_with( tenant_id="tenant1", provider="openai", credential_id=VALID_UUID @@ -321,7 +345,7 @@ class TestModelProviderCredentialApi: return_value=None, ) as get_provider_credential, ): - result = method(api, "tenant1", provider="openai") + result = method(api, ParserCredentialId.model_validate(_payload()), "tenant1", provider="openai") get_provider_credential.assert_called_once_with(tenant_id="tenant1", provider="openai", credential_id=None) assert result == {"credentials": None} @@ -332,7 +356,7 @@ class TestModelProviderCredentialApi: with app.test_request_context(f"/?credential_id={INVALID_UUID}"): with pytest.raises(ValidationError): - method(api, "tenant1", provider="openai") + method(api, ParserCredentialId.model_validate(_payload()), "tenant1", provider="openai") def test_post_create_success(self, app: Flask): api = ModelProviderCredentialApi() @@ -347,7 +371,9 @@ class TestModelProviderCredentialApi: return_value=None, ) as create_provider_credential, ): - result, status = method(api, "tenant1", provider="openai") + result, status = method( + api, ParserCredentialCreate.model_validate(_payload()), "tenant1", provider="openai" + ) create_provider_credential.assert_called_once_with( tenant_id="tenant1", @@ -372,7 +398,7 @@ class TestModelProviderCredentialApi: ), ): with pytest.raises(ValueError): - method(api, "tenant1", provider="openai") + method(api, ParserCredentialCreate.model_validate(_payload()), "tenant1", provider="openai") def test_put_update_success(self, app: Flask): api = ModelProviderCredentialApi() @@ -387,7 +413,7 @@ class TestModelProviderCredentialApi: return_value=None, ) as update_provider_credential, ): - result = method(api, "tenant1", provider="openai") + result = method(api, ParserCredentialUpdate.model_validate(_payload()), "tenant1", provider="openai") update_provider_credential.assert_called_once_with( tenant_id="tenant1", @@ -406,7 +432,7 @@ class TestModelProviderCredentialApi: with app.test_request_context("/", json=payload): with pytest.raises(ValidationError): - method(api, "tenant1", provider="openai") + method(api, ParserCredentialUpdate.model_validate(_payload()), "tenant1", provider="openai") def test_delete_success(self, app: Flask): api = ModelProviderCredentialApi() @@ -421,7 +447,9 @@ class TestModelProviderCredentialApi: return_value=None, ) as remove_provider_credential, ): - result, status = method(api, "tenant1", provider="openai") + result, status = method( + api, ParserCredentialDelete.model_validate(_payload()), "tenant1", provider="openai" + ) remove_provider_credential.assert_called_once_with( tenant_id="tenant1", provider="openai", credential_id=VALID_UUID @@ -444,7 +472,7 @@ class TestModelProviderCredentialSwitchApi: return_value=None, ) as switch_active_provider_credential, ): - result = method(api, "tenant1", provider="openai") + result = method(api, ParserCredentialSwitch.model_validate(_payload()), "tenant1", provider="openai") switch_active_provider_credential.assert_called_once_with( tenant_id="tenant1", @@ -461,7 +489,7 @@ class TestModelProviderCredentialSwitchApi: with app.test_request_context("/", json=payload): with pytest.raises(ValidationError): - method(api, "tenant1", provider="openai") + method(api, ParserCredentialSwitch.model_validate(_payload()), "tenant1", provider="openai") class TestModelProviderValidateApi: @@ -478,7 +506,7 @@ class TestModelProviderValidateApi: return_value=None, ) as validate_provider_credentials, ): - result = method(api, "tenant1", provider="openai") + result = method(api, ParserCredentialValidate.model_validate(_payload()), "tenant1", provider="openai") validate_provider_credentials.assert_called_once_with( tenant_id="tenant1", provider="openai", credentials={"a": "b"} @@ -498,7 +526,7 @@ class TestModelProviderValidateApi: side_effect=CredentialsValidateFailedError("bad"), ), ): - result = method(api, "tenant1", provider="openai") + result = method(api, ParserCredentialValidate.model_validate(_payload()), "tenant1", provider="openai") assert result == {"result": "error", "error": "bad"} @@ -549,7 +577,7 @@ class TestPreferredProviderTypeUpdateApi: return_value=None, ) as switch_preferred_provider, ): - result = method(api, "tenant1", provider="openai") + result = method(api, ParserPreferredProviderType.model_validate(_payload()), "tenant1", provider="openai") switch_preferred_provider.assert_called_once_with( tenant_id="tenant1", provider="openai", preferred_provider_type="custom" @@ -564,7 +592,7 @@ class TestPreferredProviderTypeUpdateApi: with app.test_request_context("/", json=payload): with pytest.raises(ValidationError): - method(api, "tenant1", provider="openai") + method(api, ParserPreferredProviderType.model_validate(_payload()), "tenant1", provider="openai") class TestModelProviderPaymentCheckoutUrlApi: @@ -618,3 +646,57 @@ class TestModelProviderPaymentCheckoutUrlApi: api.get(provider="anthropic") get_model_provider_payment_link.assert_not_called() + + +class TestModelValidateDecorator: + """The tests above unwrap the view, so this is what covers the decorators themselves.""" + + EMPTY_BODY: dict[str, object] = {} + EMPTY_KWARGS: dict[str, object] = {} + + @pytest.mark.parametrize( + ("verb", "url", "body", "call"), + [ + ("GET", "/?model_type=not-a-model-type", None, lambda: ModelProviderListApi().get()), + ( + "GET", + "/?credential_id=not-a-uuid", + None, + lambda: ModelProviderCredentialApi().get(provider="openai"), + ), + ("POST", "/", EMPTY_BODY, lambda: ModelProviderCredentialApi().post(provider="openai")), + ("PUT", "/", EMPTY_BODY, lambda: ModelProviderCredentialApi().put(provider="openai")), + ("DELETE", "/", EMPTY_BODY, lambda: ModelProviderCredentialApi().delete(provider="openai")), + ("POST", "/", EMPTY_BODY, lambda: ModelProviderCredentialSwitchApi().post(provider="openai")), + ("POST", "/", EMPTY_BODY, lambda: ModelProviderValidateApi().post(provider="openai")), + ("POST", "/", EMPTY_BODY, lambda: PreferredProviderTypeUpdateApi().post(provider="openai")), + ], + ) + def test_invalid_input_is_rejected_before_the_handler_runs( + self, + app: Flask, + verb: str, + url: str, + body: dict[str, object] | None, + call: Callable[[], object], + ) -> None: + account = make_account() + account.role = TenantAccountRole.OWNER + + with ( + app.test_request_context(url, method=verb, json=body), + config_overrides_context(LOGIN_DISABLED=True, RBAC_ENABLED=False), + patch("controllers.console.wraps._is_setup_completed", return_value=True), + patch( + "controllers.console.wraps.current_account_with_tenant", + return_value=(account, "tenant1"), + ), + patch("libs.login.current_user", SimpleNamespace(_get_current_object=lambda: account)), + patch("controllers.console.workspace.model_providers.ModelProviderService") as service, + ): + g._login_user = account + with pytest.raises(UnprocessableEntity) as exc_info: + call() + + assert exc_info.value.code == 422 + service.assert_not_called() diff --git a/api/tests/unit_tests/controllers/openapi/test_app_run_rate_limit.py b/api/tests/unit_tests/controllers/openapi/test_app_run_rate_limit.py index d9c468d0a4e..d2787a57591 100644 --- a/api/tests/unit_tests/controllers/openapi/test_app_run_rate_limit.py +++ b/api/tests/unit_tests/controllers/openapi/test_app_run_rate_limit.py @@ -8,8 +8,12 @@ import pytest from werkzeug.exceptions import TooManyRequests from controllers.openapi.app_run import _translate_service_errors +from controllers.service_api.app.error import TriggerWorkflowServiceModeUnavailableError from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.errors.error import AppInvokeQuotaExceededError +from services.errors.app import ( + TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError, +) from services.errors.llm import InvokeRateLimitError @@ -27,3 +31,11 @@ def test_translate_maps_workflow_quota_to_rate_limit_error(): raise InvokeRateLimitError("workflow quota exhausted") assert exc.value.error_code == "rate_limit_error" assert exc.value.code == 429 + + +def test_translate_maps_trigger_workflow_to_stable_unavailable_error(): + with pytest.raises(TriggerWorkflowServiceModeUnavailableError) as exc: + with _translate_service_errors(): + raise TriggerWorkflowServiceModeUnavailableServiceError() + assert exc.value.error_code == "trigger_workflow_service_mode_unavailable" + assert exc.value.code == 403 diff --git a/api/tests/unit_tests/controllers/service_api/app/test_annotation.py b/api/tests/unit_tests/controllers/service_api/app/test_annotation.py index 4bd305008bc..b27c478ba6d 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_annotation.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_annotation.py @@ -229,7 +229,8 @@ class TestAnnotationListApi: handler = unwrap(api.get) app_model = SimpleNamespace(id="app") with app.test_request_context("/apps/annotations", method="GET"): - response = handler(api, MagicMock(), app_model=app_model) + query = AnnotationListQuery.model_validate(request.args.to_dict(flat=True)) + response = handler(api, query, MagicMock(), app_model=app_model) assert response["page"] == 1 assert response["limit"] == 20 session = get_mock.call_args.args[-1] @@ -244,7 +245,8 @@ class TestAnnotationListApi: handler = unwrap(api.get) app_model = SimpleNamespace(id="app") with app.test_request_context("/apps/annotations?page=2&limit=5&keyword=refund", method="GET"): - response = handler(api, MagicMock(), app_model=app_model) + query = AnnotationListQuery.model_validate(request.args.to_dict(flat=True)) + response = handler(api, query, MagicMock(), app_model=app_model) assert response["total"] == 1 assert response["page"] == 2 assert response["limit"] == 5 @@ -259,11 +261,12 @@ class TestAnnotationListApi: get_mock = Mock(return_value=([], 0)) monkeypatch.setattr(AppAnnotationService, "get_annotation_list_by_app_id", get_mock) api = AnnotationListApi() - handler = unwrap(api.get) - app_model = SimpleNamespace(id="app") + # The parse moved into @model_validate, so build the query the way the decorator does and + # assert it is what rejects the input, before the view is ever reached. with app.test_request_context(f"/apps/annotations?{query_string}", method="GET"): with pytest.raises(ValidationError): - handler(api, MagicMock(), app_model=app_model) + AnnotationListQuery.model_validate(request.args.to_dict(flat=True)) + assert unwrap(api.get) is not api.get get_mock.assert_not_called() def test_create(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py index e163ebf4be5..17feffd4400 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py @@ -539,7 +539,12 @@ class TestConversationApiController: with app.test_request_context("/conversations", method="GET"): with pytest.raises(NotChatAppError): - handler(api, app_model=app_model, end_user=end_user) + handler( + api, + ConversationListQuery.model_validate(request.args.to_dict(flat=True)), + app_model=app_model, + end_user=end_user, + ) def test_list_last_not_found( self, @@ -566,7 +571,12 @@ class TestConversationApiController: method="GET", ): with pytest.raises(NotFound): - handler(api, app_model=app_model, end_user=end_user) + handler( + api, + ConversationListQuery.model_validate(request.args.to_dict(flat=True)), + app_model=app_model, + end_user=end_user, + ) class TestConversationDetailApiController: @@ -647,6 +657,7 @@ class TestConversationVariablesApiController: with pytest.raises(NotChatAppError): handler( api, + ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True)), app_model=app_model, end_user=end_user, conversation_id="00000000-0000-0000-0000-000000000001", @@ -671,6 +682,7 @@ class TestConversationVariablesApiController: with pytest.raises(NotFound): handler( api, + ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True)), app_model=app_model, end_user=end_user, conversation_id="00000000-0000-0000-0000-000000000001", @@ -708,6 +720,7 @@ class TestConversationVariablesApiController: ): result = handler( api, + ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True)), app_model=app_model, end_user=end_user, conversation_id="00000000-0000-0000-0000-000000000001", diff --git a/api/tests/unit_tests/controllers/service_api/app/test_message.py b/api/tests/unit_tests/controllers/service_api/app/test_message.py index a1e1f699bbe..95c6a49b186 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_message.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_message.py @@ -415,9 +415,16 @@ class TestMessageListApi: app_model = SimpleNamespace(mode=AppMode.COMPLETION.value) end_user = SimpleNamespace() - with app.test_request_context("/messages?conversation_id=cid", method="GET"): + # @model_validate parses ahead of the app-mode guard, so the id has to be well-formed to + # reach the branch this test is about. + with app.test_request_context("/messages?conversation_id=00000000-0000-0000-0000-000000000001", method="GET"): with pytest.raises(NotChatAppError): - handler(api, app_model=app_model, end_user=end_user) + handler( + api, + MessageListQuery.model_validate(request.args.to_dict(flat=True)), + app_model=app_model, + end_user=end_user, + ) def test_conversation_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( @@ -436,7 +443,12 @@ class TestMessageListApi: method="GET", ): with pytest.raises(NotFound): - handler(api, app_model=app_model, end_user=end_user) + handler( + api, + MessageListQuery.model_validate(request.args.to_dict(flat=True)), + app_model=app_model, + end_user=end_user, + ) def test_first_message_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( @@ -455,7 +467,12 @@ class TestMessageListApi: method="GET", ): with pytest.raises(NotFound): - handler(api, app_model=app_model, end_user=end_user) + handler( + api, + MessageListQuery.model_validate(request.args.to_dict(flat=True)), + app_model=app_model, + end_user=end_user, + ) class TestMessageFeedbackApi: @@ -503,7 +520,9 @@ class TestAppGetFeedbacksApi: app_model = SimpleNamespace() with app.test_request_context("/app/feedbacks?page=1&limit=20", method="GET"): - response = handler(api, app_model=app_model) + response = handler( + api, FeedbackListQuery.model_validate(request.args.to_dict(flat=True)), app_model=app_model + ) assert response == {"data": [feedback]} diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py index f91a9ca35f2..9dd436d8795 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py @@ -28,7 +28,11 @@ from sqlalchemy.engine import Engine from sqlalchemy.orm import Session, sessionmaker from werkzeug.exceptions import BadRequest, NotFound -from controllers.service_api.app.error import NotWorkflowAppError, WorkflowVersionExecutionNotAllowedError +from controllers.service_api.app.error import ( + NotWorkflowAppError, + TriggerWorkflowServiceModeUnavailableError, + WorkflowVersionExecutionNotAllowedError, +) from controllers.service_api.app.workflow import ( AppQueueManager, GraphEngineManager, @@ -51,7 +55,13 @@ from models.model import App, AppMode, EndUser from models.workflow import WorkflowAppLog, WorkflowAppLogCreatedFrom, WorkflowRun, WorkflowType from services.app_generate_service import AppGenerateService from services.billing_service import BillingService -from services.errors.app import IsDraftWorkflowError, WorkflowNotFoundError +from services.errors.app import ( + IsDraftWorkflowError, + WorkflowNotFoundError, +) +from services.errors.app import ( + TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError, +) from services.errors.llm import InvokeRateLimitError from services.workflow_app_service import WorkflowAppService @@ -582,6 +592,32 @@ class TestWorkflowRunApi: with pytest.raises(InvokeRateLimitHttpError): handler(api, session=sqlite_session, app_model=app_model, end_user=end_user) + def test_trigger_workflow_returns_stable_unavailable_error( + self, + app: Flask, + monkeypatch: pytest.MonkeyPatch, + sqlite_session: Session, + ) -> None: + monkeypatch.setattr( + AppGenerateService, + "generate", + Mock(side_effect=TriggerWorkflowServiceModeUnavailableServiceError()), + ) + api = WorkflowRunApi() + handler = unwrap(api.post) + + with app.test_request_context("/workflows/run", method="POST", json={"inputs": {}}): + with pytest.raises(TriggerWorkflowServiceModeUnavailableError) as exc_info: + handler( + api, + session=sqlite_session, + app_model=_make_app_model(), + end_user=_make_end_user(), + ) + + assert exc_info.value.code == 403 + assert exc_info.value.error_code == "trigger_workflow_service_mode_unavailable" + def test_sandbox_billing_does_not_gate_default_workflow_run( self, app: Flask, @@ -614,6 +650,33 @@ class TestWorkflowRunApi: class TestWorkflowRunByIdApi: + def test_trigger_workflow_version_returns_stable_unavailable_error( + self, + app: Flask, + monkeypatch: pytest.MonkeyPatch, + sqlite_session: Session, + ) -> None: + monkeypatch.setattr( + AppGenerateService, + "generate", + Mock(side_effect=TriggerWorkflowServiceModeUnavailableServiceError()), + ) + api = WorkflowRunByIdApi() + handler = unwrap(api.post) + + with app.test_request_context("/workflows/w1/run", method="POST", json={"inputs": {}}): + with pytest.raises(TriggerWorkflowServiceModeUnavailableError) as exc_info: + handler( + api, + session=sqlite_session, + app_model=_make_app_model(), + end_user=_make_end_user(), + workflow_id=str(uuid.uuid4()), + ) + + assert exc_info.value.code == 403 + assert exc_info.value.error_code == "trigger_workflow_service_mode_unavailable" + def test_rejects_sandbox_plan_with_upgrade_error( self, app: Flask, diff --git a/api/tests/unit_tests/controllers/web/test_workflow.py b/api/tests/unit_tests/controllers/web/test_workflow.py index 2013b1e8db5..711976ed441 100644 --- a/api/tests/unit_tests/controllers/web/test_workflow.py +++ b/api/tests/unit_tests/controllers/web/test_workflow.py @@ -11,11 +11,15 @@ from controllers.web.error import ( NotWorkflowAppError, ProviderNotInitializeError, ProviderQuotaExceededError, + TriggerWorkflowServiceModeUnavailableError, ) from controllers.web.workflow import WorkflowRunApi, WorkflowTaskStopApi from core.errors.error import ProviderTokenNotInitError, QuotaExceededError from models.enums import EndUserType from models.model import App, AppMode, EndUser +from services.errors.app import ( + TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError, +) def _workflow_app() -> App: @@ -68,6 +72,26 @@ class TestWorkflowRunApi: with pytest.raises(ProviderNotInitializeError): WorkflowRunApi().post(_workflow_app(), _end_user()) + @patch( + "controllers.web.workflow.AppGenerateService.generate", + side_effect=TriggerWorkflowServiceModeUnavailableServiceError(), + ) + @patch("controllers.web.workflow.web_ns") + def test_trigger_workflow_returns_stable_unavailable_error( + self, + mock_ns: MagicMock, + mock_gen: MagicMock, + app: Flask, + ) -> None: + mock_ns.payload = {"inputs": {}} + + with app.test_request_context("/workflows/run", method="POST"): + with pytest.raises(TriggerWorkflowServiceModeUnavailableError) as exc_info: + WorkflowRunApi().post(_workflow_app(), _end_user()) + + assert exc_info.value.code == 403 + assert exc_info.value.error_code == "trigger_workflow_service_mode_unavailable" + @patch( "controllers.web.workflow.AppGenerateService.generate", side_effect=QuotaExceededError(), diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py index 2f69b1ed877..b99e0d23394 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py @@ -1,12 +1,15 @@ from __future__ import annotations +import json import logging from contextlib import contextmanager +from decimal import Decimal from types import SimpleNamespace from unittest.mock import MagicMock import pytest from pydantic import BaseModel, ValidationError +from sqlalchemy import Engine, event from sqlalchemy.orm import Session from constants import UUID_NIL @@ -21,20 +24,99 @@ from core.app.apps.exc import GenerateTaskStoppedError from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom from core.ops.ops_trace_manager import TraceQueueManager from libs.datetime_utils import naive_utc_now -from models.enums import MessageStatus -from models.model import AppMode +from models.account import Account +from models.enums import ConversationFromSource, EndUserType, MessageStatus +from models.model import App, AppMode, Conversation, EndUser, Message +from models.workflow import Workflow, WorkflowType from tests.unit_tests.config_override import apply_config_overrides +def _make_app(*, app_id: str = "app", tenant_id: str = "tenant") -> App: + return App( + id=app_id, + tenant_id=tenant_id, + name="Advanced Chat App", + mode=AppMode.ADVANCED_CHAT, + enable_site=False, + enable_api=False, + ) + + +def _make_workflow( + *, + workflow_id: str = "workflow-id", + tenant_id: str = "tenant", + app_id: str = "app", + features: dict[str, object] | None = None, +) -> Workflow: + return Workflow( + id=workflow_id, + tenant_id=tenant_id, + app_id=app_id, + type=WorkflowType.CHAT, + version=Workflow.VERSION_DRAFT, + graph="{}", + features=json.dumps(features or {}), + created_by="user", + ) + + +def _make_account(*, account_id: str = "user-id") -> Account: + account = Account(name="Advanced Chat User", email=f"{account_id}@example.com") + account.id = account_id + return account + + +def _make_end_user(*, end_user_id: str = "end-user-id", session_id: str = "session-id") -> EndUser: + return EndUser( + id=end_user_id, + tenant_id="tenant", + app_id="app", + type=EndUserType.BROWSER, + session_id=session_id, + ) + + +def _make_conversation(*, conversation_id: str = "conversation-id", app_id: str = "app") -> Conversation: + return Conversation( + id=conversation_id, + app_id=app_id, + mode=AppMode.ADVANCED_CHAT, + name="Advanced Chat Conversation", + inputs={}, + from_source=ConversationFromSource.API, + ) + + +def _make_message( + *, message_id: str = "message-id", conversation_id: str = "conversation-id", app_id: str = "app" +) -> Message: + return Message( + id=message_id, + app_id=app_id, + conversation_id=conversation_id, + inputs={}, + query="hello", + message={}, + answer="", + status=MessageStatus.NORMAL, + message_unit_price=Decimal(0), + answer_unit_price=Decimal(0), + currency="USD", + from_source=ConversationFromSource.API, + created_at=naive_utc_now(), + ) + + class TestAdvancedChatAppGeneratorValidation: def test_generate_requires_query(self, unbound_session: Session): generator = AdvancedChatAppGenerator() with pytest.raises(ValueError, match="query is required"): generator.generate( - app_model=SimpleNamespace(), - workflow=SimpleNamespace(), - user=SimpleNamespace(), + app_model=_make_app(), + workflow=_make_workflow(), + user=_make_account(), args={"inputs": {}}, invoke_from=InvokeFrom.WEB_APP, workflow_run_id="run-id", @@ -47,9 +129,9 @@ class TestAdvancedChatAppGeneratorValidation: with pytest.raises(ValueError, match="query must be a string"): generator.generate( - app_model=SimpleNamespace(), - workflow=SimpleNamespace(), - user=SimpleNamespace(), + app_model=_make_app(), + workflow=_make_workflow(), + user=_make_account(), args={"inputs": {}, "query": 123}, invoke_from=InvokeFrom.WEB_APP, workflow_run_id="run-id", @@ -62,10 +144,10 @@ class TestAdvancedChatAppGeneratorValidation: with pytest.raises(ValueError, match="node_id is required"): generator.single_iteration_generate( - app_model=SimpleNamespace(), - workflow=SimpleNamespace(), + app_model=_make_app(), + workflow=_make_workflow(), node_id="", - user=SimpleNamespace(), + user=_make_account(), args={"inputs": {}}, streaming=False, session=unbound_session, @@ -73,10 +155,10 @@ class TestAdvancedChatAppGeneratorValidation: with pytest.raises(ValueError, match="inputs is required"): generator.single_iteration_generate( - app_model=SimpleNamespace(), - workflow=SimpleNamespace(), + app_model=_make_app(), + workflow=_make_workflow(), node_id="node", - user=SimpleNamespace(), + user=_make_account(), args={}, streaming=False, session=unbound_session, @@ -87,10 +169,10 @@ class TestAdvancedChatAppGeneratorValidation: with pytest.raises(ValueError, match="node_id is required"): generator.single_loop_generate( - app_model=SimpleNamespace(), - workflow=SimpleNamespace(), + app_model=_make_app(), + workflow=_make_workflow(), node_id="", - user=SimpleNamespace(), + user=_make_account(), args=SimpleNamespace(inputs={}), streaming=False, session=unbound_session, @@ -98,10 +180,10 @@ class TestAdvancedChatAppGeneratorValidation: with pytest.raises(ValueError, match="inputs is required"): generator.single_loop_generate( - app_model=SimpleNamespace(), - workflow=SimpleNamespace(), + app_model=_make_app(), + workflow=_make_workflow(), node_id="node", - user=SimpleNamespace(), + user=_make_account(), args=SimpleNamespace(inputs=None), streaming=False, session=unbound_session, @@ -120,11 +202,13 @@ class TestAdvancedChatAppGeneratorInternals: workflow_id="workflow-id", ) - def test_generate_loads_conversation_and_files(self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session): + def test_generate_loads_conversation_and_files( + self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session, sqlite_engine: Engine + ): generator = AdvancedChatAppGenerator() app_config = self._build_app_config() - conversation = SimpleNamespace(id="conversation-id") + conversation = _make_conversation() built_files: list[object] = [] build_files_called = {"called": False} captured: dict[str, object] = {} @@ -157,10 +241,7 @@ class TestAdvancedChatAppGeneratorInternals: ) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=lambda: SimpleNamespace(close=lambda: None)), - ) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.sessionmaker", lambda **kwargs: SimpleNamespace() + SimpleNamespace(engine=sqlite_engine, session=unbound_session), ) monkeypatch.setattr(generator, "_prepare_user_inputs", lambda **kwargs: kwargs["user_inputs"]) @@ -187,8 +268,8 @@ class TestAdvancedChatAppGeneratorInternals: user.id = "user-id" result = generator.generate( - app_model=SimpleNamespace(id="app", tenant_id="tenant"), - workflow=SimpleNamespace(features_dict={}), + app_model=_make_app(), + workflow=_make_workflow(), user=user, args={ "query": "hello", @@ -238,11 +319,11 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr(generator, "_generate", _fake_generate) result = generator.resume( - app_model=SimpleNamespace(id="app-id"), - workflow=SimpleNamespace(), - user=SimpleNamespace(id="end-user-id", session_id="session-id"), - conversation=SimpleNamespace(id="conversation-id"), - message=SimpleNamespace(id="message-id"), + app_model=_make_app(app_id="app-id"), + workflow=_make_workflow(), + user=_make_end_user(), + conversation=_make_conversation(), + message=_make_message(), session=unbound_session, application_generate_entity=application_generate_entity, workflow_execution_repository=SimpleNamespace(), @@ -257,7 +338,7 @@ class TestAdvancedChatAppGeneratorInternals: assert captured_graph_runtime_state is not None def test_single_iteration_generate_builds_debug_task( - self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session + self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session, sqlite_engine: Engine ): generator = AdvancedChatAppGenerator() app_config = self._build_app_config() @@ -265,7 +346,7 @@ class TestAdvancedChatAppGeneratorInternals: prefill_calls: list[object] = [] draft_sessions: list[object] = [] var_loader = SimpleNamespace(loader="draft") - workflow = SimpleNamespace(id="workflow-id") + workflow = _make_workflow() session = unbound_session monkeypatch.setattr( @@ -281,12 +362,9 @@ class TestAdvancedChatAppGeneratorInternals: lambda **kwargs: SimpleNamespace(repo="node"), ) monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.DraftVarLoader", lambda **kwargs: var_loader) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.sessionmaker", lambda **kwargs: SimpleNamespace() - ) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=lambda: SimpleNamespace()), + SimpleNamespace(engine=sqlite_engine, session=unbound_session), ) class _DraftVarService: @@ -305,10 +383,10 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr(generator, "_generate", _fake_generate) result = generator.single_iteration_generate( - app_model=SimpleNamespace(id="app", tenant_id="tenant"), + app_model=_make_app(), workflow=workflow, node_id="node-1", - user=SimpleNamespace(id="user-id"), + user=_make_account(), args={"inputs": {"foo": "bar"}, "trace_session_id": "session-1"}, streaming=False, session=session, @@ -322,14 +400,16 @@ class TestAdvancedChatAppGeneratorInternals: assert captured["application_generate_entity"].single_iteration_run.node_id == "node-1" assert captured["application_generate_entity"].extras["trace_session_id"] == "session-1" - def test_single_loop_generate_builds_debug_task(self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session): + def test_single_loop_generate_builds_debug_task( + self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session, sqlite_engine: Engine + ): generator = AdvancedChatAppGenerator() app_config = self._build_app_config() captured: dict[str, object] = {} prefill_calls: list[object] = [] draft_sessions: list[object] = [] var_loader = SimpleNamespace(loader="draft") - workflow = SimpleNamespace(id="workflow-id") + workflow = _make_workflow() session = unbound_session monkeypatch.setattr( @@ -345,12 +425,9 @@ class TestAdvancedChatAppGeneratorInternals: lambda **kwargs: SimpleNamespace(repo="node"), ) monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.DraftVarLoader", lambda **kwargs: var_loader) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.sessionmaker", lambda **kwargs: SimpleNamespace() - ) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=lambda: SimpleNamespace()), + SimpleNamespace(engine=sqlite_engine, session=unbound_session), ) class _DraftVarService: @@ -369,10 +446,10 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr(generator, "_generate", _fake_generate) result = generator.single_loop_generate( - app_model=SimpleNamespace(id="app", tenant_id="tenant"), + app_model=_make_app(), workflow=workflow, node_id="node-2", - user=SimpleNamespace(id="user-id"), + user=_make_account(), args=SimpleNamespace(inputs={"foo": "bar"}, trace_session_id="session-1"), streaming=False, session=session, @@ -386,7 +463,9 @@ class TestAdvancedChatAppGeneratorInternals: assert captured["application_generate_entity"].single_loop_run.node_id == "node-2" assert captured["application_generate_entity"].extras["trace_session_id"] == "session-1" - def test_generate_internal_flow_initial_conversation_with_pause_layer(self, monkeypatch: pytest.MonkeyPatch): + def test_generate_internal_flow_initial_conversation_with_pause_layer( + self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session, sqlite_engine: Engine + ): generator = AdvancedChatAppGenerator() generator._dialogue_count = 0 app_config = self._build_app_config() @@ -405,16 +484,19 @@ class TestAdvancedChatAppGeneratorInternals: workflow_run_id="run-id", ) - workflow = SimpleNamespace(id="wf-1", tenant_id="tenant", features={"feature": True}, features_dict={}) - conversation = SimpleNamespace(id="conv-1", mode=AppMode.ADVANCED_CHAT, override_model_configs=None) - message = SimpleNamespace( - id="msg-1", - query="hello", - created_at=naive_utc_now(), - status=MessageStatus.NORMAL, - answer="", - ) - db_session = SimpleNamespace(commit=MagicMock(), refresh=MagicMock(), close=MagicMock()) + app = _make_app() + workflow = _make_workflow(workflow_id="wf-1", features={"feature": True}) + conversation = _make_conversation(conversation_id="conv-1") + message = _make_message(message_id="msg-1", conversation_id=conversation.id) + sqlite_session.add_all([app, workflow, conversation, message]) + sqlite_session.commit() + commit_count = 0 + + def _record_commit(session: Session) -> None: + nonlocal commit_count + commit_count += 1 + + event.listen(sqlite_session, "after_commit", _record_commit) captured: dict[str, object] = {} thread_data: dict[str, object] = {} init_records = MagicMock(return_value=(conversation, message)) @@ -455,7 +537,8 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.threading.Thread", _Thread) monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.db", SimpleNamespace(engine=object(), session=db_session) + "core.app.apps.advanced_chat.app_generator.db", + SimpleNamespace(engine=sqlite_engine, session=sqlite_session), ) monkeypatch.setattr(generator, "_get_draft_var_saver_factory", lambda *args, **kwargs: "draft-factory") monkeypatch.setattr( @@ -472,10 +555,10 @@ class TestAdvancedChatAppGeneratorInternals: response = generator._generate( workflow=workflow, - user=SimpleNamespace(id="user"), + user=_make_account(account_id="user"), invoke_from=InvokeFrom.WEB_APP, application_generate_entity=application_generate_entity, - session=db_session, + session=sqlite_session, workflow_execution_repository=SimpleNamespace(), workflow_node_execution_repository=SimpleNamespace(), conversation=None, @@ -490,17 +573,18 @@ class TestAdvancedChatAppGeneratorInternals: assert thread_data["join_timeout"] == 300 assert "pause-layer" in thread_data["kwargs"]["graph_engine_layers"] assert generator._dialogue_count == 3 - assert init_records.call_args.kwargs["session"] is db_session - get_thread_messages_length.assert_called_once_with(conversation.id, session=db_session) - db_session.commit.assert_called_once() - db_session.refresh.assert_called_once_with(conversation) - db_session.close.assert_called_once() + assert init_records.call_args.kwargs["session"] is sqlite_session + get_thread_messages_length.assert_called_once_with(conversation.id, session=sqlite_session) + assert commit_count == 1 + assert json.loads(conversation.override_model_configs) == {"feature": True} assert captured["draft_var_saver_factory"] == "draft-factory" assert isinstance(captured["workflow"], WorkflowSnapshot) assert isinstance(captured["conversation"], ConversationSnapshot) assert isinstance(captured["message"], MessageSnapshot) - def test_generate_internal_flow_with_existing_records_skips_init(self, monkeypatch: pytest.MonkeyPatch): + def test_generate_internal_flow_with_existing_records_skips_init( + self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session, sqlite_engine: Engine + ): generator = AdvancedChatAppGenerator() generator._dialogue_count = 0 app_config = self._build_app_config() @@ -519,16 +603,19 @@ class TestAdvancedChatAppGeneratorInternals: workflow_run_id="run-id", ) - workflow = SimpleNamespace(id="wf-2", tenant_id="tenant", features={}, features_dict={}) - conversation = SimpleNamespace(id="conv-2", mode=AppMode.ADVANCED_CHAT, override_model_configs=None) - message = SimpleNamespace( - id="msg-2", - query="hello", - created_at=naive_utc_now(), - status=MessageStatus.NORMAL, - answer="", - ) - db_session = SimpleNamespace(close=MagicMock(), commit=MagicMock(), refresh=MagicMock()) + app = _make_app() + workflow = _make_workflow(workflow_id="wf-2") + conversation = _make_conversation(conversation_id="conv-2") + message = _make_message(message_id="msg-2", conversation_id=conversation.id) + sqlite_session.add_all([app, workflow, conversation, message]) + sqlite_session.commit() + commit_count = 0 + + def _record_commit(session: Session) -> None: + nonlocal commit_count + commit_count += 1 + + event.listen(sqlite_session, "after_commit", _record_commit) init_records = MagicMock() get_thread_messages_length = MagicMock(return_value=0) thread_data: dict[str, object] = {} @@ -564,7 +651,8 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.threading.Thread", _Thread) monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.db", SimpleNamespace(engine=object(), session=db_session) + "core.app.apps.advanced_chat.app_generator.db", + SimpleNamespace(engine=sqlite_engine, session=sqlite_session), ) monkeypatch.setattr(generator, "_get_draft_var_saver_factory", lambda *args, **kwargs: "draft-factory") monkeypatch.setattr( @@ -579,10 +667,10 @@ class TestAdvancedChatAppGeneratorInternals: response = generator._generate( workflow=workflow, - user=SimpleNamespace(id="user"), + user=_make_account(account_id="user"), invoke_from=InvokeFrom.WEB_APP, application_generate_entity=application_generate_entity, - session=db_session, + session=sqlite_session, workflow_execution_repository=SimpleNamespace(), workflow_node_execution_repository=SimpleNamespace(), conversation=conversation, @@ -592,15 +680,15 @@ class TestAdvancedChatAppGeneratorInternals: assert response == {"raw": True} init_records.assert_not_called() - get_thread_messages_length.assert_called_once_with(conversation.id, session=db_session) + get_thread_messages_length.assert_called_once_with(conversation.id, session=sqlite_session) assert thread_data["started"] is True assert thread_data["joined"] is True assert thread_data["join_timeout"] == 300 - db_session.commit.assert_not_called() - db_session.refresh.assert_not_called() - db_session.close.assert_called_once() + assert commit_count == 0 - def test_generate_worker_raises_when_workflow_not_found(self, monkeypatch: pytest.MonkeyPatch): + def test_generate_worker_raises_when_workflow_not_found( + self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session, sqlite_engine: Engine + ): generator = AdvancedChatAppGenerator() generator._dialogue_count = 1 app_config = self._build_app_config() @@ -619,8 +707,8 @@ class TestAdvancedChatAppGeneratorInternals: workflow_run_id="run-id", ) - generator._get_conversation = MagicMock(return_value=SimpleNamespace(id="conv")) - generator._get_message = MagicMock(return_value=SimpleNamespace(id="msg")) + generator._get_conversation = MagicMock(return_value=_make_conversation(conversation_id="conv")) + generator._get_message = MagicMock(return_value=_make_message(message_id="msg", conversation_id="conv")) @contextmanager def _fake_context(*args, **kwargs): @@ -628,20 +716,9 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.preserve_flask_contexts", _fake_context) - class _Session: - def __init__(self, *args, **kwargs): - self.scalar = MagicMock(return_value=None) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False - - monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.Session", _Session) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=SimpleNamespace(close=lambda: None)), + SimpleNamespace(engine=sqlite_engine, session=sqlite_session), ) with pytest.raises(ValueError, match="Workflow not found"): @@ -659,7 +736,9 @@ class TestAdvancedChatAppGeneratorInternals: graph_runtime_state=None, ) - def test_generate_worker_raises_when_app_not_found_for_internal_call(self, monkeypatch: pytest.MonkeyPatch): + def test_generate_worker_raises_when_app_not_found_for_internal_call( + self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session, sqlite_engine: Engine + ): generator = AdvancedChatAppGenerator() generator._dialogue_count = 1 app_config = self._build_app_config() @@ -678,8 +757,10 @@ class TestAdvancedChatAppGeneratorInternals: workflow_run_id="run-id", ) - generator._get_conversation = MagicMock(return_value=SimpleNamespace(id="conv")) - generator._get_message = MagicMock(return_value=SimpleNamespace(id="msg")) + generator._get_conversation = MagicMock(return_value=_make_conversation(conversation_id="conv")) + generator._get_message = MagicMock(return_value=_make_message(message_id="msg", conversation_id="conv")) + sqlite_session.add(_make_workflow()) + sqlite_session.commit() @contextmanager def _fake_context(*args, **kwargs): @@ -687,25 +768,9 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.preserve_flask_contexts", _fake_context) - class _Session: - def __init__(self, *args, **kwargs): - self.scalar = MagicMock( - side_effect=[ - SimpleNamespace(id="workflow-id", tenant_id="tenant", app_id="app"), - None, - ] - ) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False - - monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.Session", _Session) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=SimpleNamespace(close=lambda: None)), + SimpleNamespace(engine=sqlite_engine, session=sqlite_session), ) with pytest.raises(ValueError, match="App not found"): @@ -723,7 +788,9 @@ class TestAdvancedChatAppGeneratorInternals: graph_runtime_state=None, ) - def test_generate_worker_handles_stopped_error(self, monkeypatch: pytest.MonkeyPatch): + def test_generate_worker_handles_stopped_error( + self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session, sqlite_engine: Engine + ): generator = AdvancedChatAppGenerator() generator._dialogue_count = 1 app_config = self._build_app_config() @@ -743,8 +810,8 @@ class TestAdvancedChatAppGeneratorInternals: ) queue_manager = MagicMock() - generator._get_conversation = MagicMock(return_value=SimpleNamespace(id="conv")) - generator._get_message = MagicMock(return_value=SimpleNamespace(id="msg")) + generator._get_conversation = MagicMock(return_value=_make_conversation(conversation_id="conv")) + generator._get_message = MagicMock(return_value=_make_message(message_id="msg", conversation_id="conv")) @contextmanager def _fake_context(*args, **kwargs): @@ -752,22 +819,8 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.preserve_flask_contexts", _fake_context) - workflow = SimpleNamespace(id="workflow-id", tenant_id="tenant", app_id="app") - - class _Session: - def __init__(self, *args, **kwargs): - self.scalar = MagicMock( - side_effect=[ - workflow, - SimpleNamespace(id="app"), - ] - ) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False + sqlite_session.add_all([_make_app(), _make_workflow()]) + sqlite_session.commit() class _Runner: def __init__(self, **kwargs): @@ -776,13 +829,12 @@ class TestAdvancedChatAppGeneratorInternals: def run(self): raise GenerateTaskStoppedError() - monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.Session", _Session) monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.AdvancedChatAppRunner", _Runner) restore_workflow_run_graph = MagicMock() monkeypatch.setattr(generator, "_restore_workflow_run_graph", restore_workflow_run_graph) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=SimpleNamespace(close=lambda: None)), + SimpleNamespace(engine=sqlite_engine, session=sqlite_session), ) generator._generate_worker( @@ -800,10 +852,12 @@ class TestAdvancedChatAppGeneratorInternals: ) queue_manager.publish_error.assert_not_called() - assert restore_workflow_run_graph.call_args.kwargs["workflow"] is workflow + assert restore_workflow_run_graph.call_args.kwargs["workflow"].id == "workflow-id" assert restore_workflow_run_graph.call_args.kwargs["workflow_run_id"] == "run-id" - def test_generate_worker_handles_validation_error(self, monkeypatch: pytest.MonkeyPatch): + def test_generate_worker_handles_validation_error( + self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session, sqlite_engine: Engine + ): generator = AdvancedChatAppGenerator() generator._dialogue_count = 1 app_config = self._build_app_config() @@ -833,8 +887,10 @@ class TestAdvancedChatAppGeneratorInternals: raise AssertionError("validation error should be created") queue_manager = MagicMock() - generator._get_conversation = MagicMock(return_value=SimpleNamespace(id="conv")) - generator._get_message = MagicMock(return_value=SimpleNamespace(id="msg")) + generator._get_conversation = MagicMock(return_value=_make_conversation(conversation_id="conv")) + generator._get_message = MagicMock(return_value=_make_message(message_id="msg", conversation_id="conv")) + sqlite_session.add_all([_make_app(), _make_workflow()]) + sqlite_session.commit() @contextmanager def _fake_context(*args, **kwargs): @@ -842,21 +898,6 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.preserve_flask_contexts", _fake_context) - class _Session: - def __init__(self, *args, **kwargs): - self.scalar = MagicMock( - side_effect=[ - SimpleNamespace(id="workflow-id", tenant_id="tenant", app_id="app"), - SimpleNamespace(id="app"), - ] - ) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False - class _Runner: def __init__(self, **kwargs): _ = kwargs @@ -864,11 +905,10 @@ class TestAdvancedChatAppGeneratorInternals: def run(self): raise validation_error - monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.Session", _Session) monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.AdvancedChatAppRunner", _Runner) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=SimpleNamespace(close=lambda: None)), + SimpleNamespace(engine=sqlite_engine, session=sqlite_session), ) generator._generate_worker( @@ -887,8 +927,12 @@ class TestAdvancedChatAppGeneratorInternals: queue_manager.publish_error.assert_called_once() - def test_generate_worker_handles_value_and_unknown_errors(self, monkeypatch: pytest.MonkeyPatch): + def test_generate_worker_handles_value_and_unknown_errors( + self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session, sqlite_engine: Engine + ): app_config = self._build_app_config() + sqlite_session.add_all([_make_app(), _make_workflow()]) + sqlite_session.commit() @contextmanager def _fake_context(*args, **kwargs): @@ -922,26 +966,10 @@ class TestAdvancedChatAppGeneratorInternals: ) queue_manager = MagicMock() - generator._get_conversation = MagicMock(return_value=SimpleNamespace(id="conv")) - generator._get_message = MagicMock(return_value=SimpleNamespace(id="msg")) - - class _Session: - def __init__(self, *args, **kwargs): - self.scalar = MagicMock( - side_effect=[ - SimpleNamespace(id="workflow-id", tenant_id="tenant", app_id="app"), - SimpleNamespace(id="app"), - ] - ) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False + generator._get_conversation = MagicMock(return_value=_make_conversation(conversation_id="conv")) + generator._get_message = MagicMock(return_value=_make_message(message_id="msg", conversation_id="conv")) monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.preserve_flask_contexts", _fake_context) - monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.Session", _Session) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.AdvancedChatAppRunner", _make_runner(raised_error), @@ -949,7 +977,7 @@ class TestAdvancedChatAppGeneratorInternals: apply_config_overrides(monkeypatch, DEBUG=True) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=SimpleNamespace(close=lambda: None)), + SimpleNamespace(engine=sqlite_engine, session=sqlite_session), ) generator._generate_worker( @@ -1019,7 +1047,7 @@ class TestAdvancedChatAppGeneratorInternals: status=MessageStatus.NORMAL, answer="", ), - user=SimpleNamespace(), + user=_make_account(), draft_var_saver_factory=lambda **kwargs: None, stream=False, ) @@ -1067,14 +1095,16 @@ class TestAdvancedChatAppGeneratorInternals: status=MessageStatus.NORMAL, answer="", ), - user=SimpleNamespace(), + user=_make_account(), draft_var_saver_factory=lambda **kwargs: None, stream=False, ) assert "Failed to process generate task pipeline, conversation_id: conv" in caplog.messages - def test_generate_worker_handles_invoke_auth_error(self, monkeypatch: pytest.MonkeyPatch): + def test_generate_worker_handles_invoke_auth_error( + self, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session, sqlite_engine: Engine + ): generator = AdvancedChatAppGenerator() generator._dialogue_count = 1 @@ -1102,8 +1132,10 @@ class TestAdvancedChatAppGeneratorInternals: queue_manager = MagicMock() - generator._get_conversation = MagicMock(return_value=SimpleNamespace(id="conv", mode=AppMode.ADVANCED_CHAT)) - generator._get_message = MagicMock(return_value=SimpleNamespace(id="msg")) + generator._get_conversation = MagicMock(return_value=_make_conversation(conversation_id="conv")) + generator._get_message = MagicMock(return_value=_make_message(message_id="msg", conversation_id="conv")) + sqlite_session.add_all([_make_app(), _make_workflow(), _make_end_user()]) + sqlite_session.commit() class _Runner: def __init__(self, **kwargs) -> None: @@ -1122,26 +1154,9 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.preserve_flask_contexts", _fake_context) - class _Session: - def __init__(self, *args, **kwargs): - self.scalar = MagicMock( - side_effect=[ - SimpleNamespace(id="workflow-id", tenant_id="tenant", app_id="app"), - SimpleNamespace(id="end-user-id", session_id="session-id"), - SimpleNamespace(id="app"), - ] - ) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False - - monkeypatch.setattr("core.app.apps.advanced_chat.app_generator.Session", _Session) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=SimpleNamespace(close=lambda: None)), + SimpleNamespace(engine=sqlite_engine, session=sqlite_session), ) generator._generate_worker( @@ -1160,88 +1175,8 @@ class TestAdvancedChatAppGeneratorInternals: assert queue_manager.publish_error.called - def test_generate_debugger_enables_retrieve_source(self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session): - generator = AdvancedChatAppGenerator() - - app_config = WorkflowUIBasedAppConfig( - tenant_id="tenant", - app_id="app", - app_mode=AppMode.ADVANCED_CHAT, - additional_features=AppAdditionalFeatures(), - variables=[], - workflow_id="workflow-id", - ) - - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.AdvancedChatAppConfigManager.get_app_config", - lambda app_model, workflow: app_config, - ) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.FileUploadConfigManager.convert", - lambda features_dict, is_vision=False: None, - ) - DummyTraceQueueManager = type( - "_DummyTraceQueueManager", - (TraceQueueManager,), - { - "__init__": lambda self, app_id=None, user_id=None: ( - setattr(self, "app_id", app_id) or setattr(self, "user_id", user_id) - ) - }, - ) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.TraceQueueManager", - DummyTraceQueueManager, - ) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.DifyCoreRepositoryFactory.create_workflow_execution_repository", - lambda **kwargs: SimpleNamespace(), - ) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.DifyCoreRepositoryFactory.create_workflow_node_execution_repository", - lambda **kwargs: SimpleNamespace(), - ) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=SimpleNamespace(close=lambda: None)), - ) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.sessionmaker", - lambda **kwargs: SimpleNamespace(), - ) - - captured = {} - - def _fake_generate(**kwargs): - captured.update(kwargs) - return {"ok": True} - - monkeypatch.setattr(generator, "_generate", _fake_generate) - - app_model = SimpleNamespace(id="app", tenant_id="tenant") - workflow = SimpleNamespace(features_dict={}) - from models import Account - - user = Account(name="Tester", email="tester@example.com") - user.id = "user" - - result = generator.generate( - app_model=app_model, - workflow=workflow, - user=user, - args={"query": "hello\x00", "inputs": {}}, - invoke_from=InvokeFrom.DEBUGGER, - workflow_run_id="run-id", - streaming=False, - session=unbound_session, - ) - - assert result == {"ok": True} - assert app_config.additional_features.show_retrieve_source is True - assert captured["application_generate_entity"].query == "hello" - - def test_generate_service_api_sets_parent_message_id( - self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session + def test_generate_debugger_enables_retrieve_source( + self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session, sqlite_engine: Engine ): generator = AdvancedChatAppGenerator() @@ -1285,11 +1220,7 @@ class TestAdvancedChatAppGeneratorInternals: ) monkeypatch.setattr( "core.app.apps.advanced_chat.app_generator.db", - SimpleNamespace(engine=object(), session=SimpleNamespace(close=lambda: None)), - ) - monkeypatch.setattr( - "core.app.apps.advanced_chat.app_generator.sessionmaker", - lambda **kwargs: SimpleNamespace(), + SimpleNamespace(engine=sqlite_engine, session=unbound_session), ) captured = {} @@ -1300,12 +1231,84 @@ class TestAdvancedChatAppGeneratorInternals: monkeypatch.setattr(generator, "_generate", _fake_generate) - app_model = SimpleNamespace(id="app", tenant_id="tenant") - workflow = SimpleNamespace(features_dict={}) - from models.model import EndUser + app_model = _make_app() + workflow = _make_workflow() + user = _make_account(account_id="user") - user = EndUser(tenant_id="tenant", type="session", name="tester", session_id="session") - user.id = "end-user" + result = generator.generate( + app_model=app_model, + workflow=workflow, + user=user, + args={"query": "hello\x00", "inputs": {}}, + invoke_from=InvokeFrom.DEBUGGER, + workflow_run_id="run-id", + streaming=False, + session=unbound_session, + ) + + assert result == {"ok": True} + assert app_config.additional_features.show_retrieve_source is True + assert captured["application_generate_entity"].query == "hello" + + def test_generate_service_api_sets_parent_message_id( + self, monkeypatch: pytest.MonkeyPatch, unbound_session: Session, sqlite_engine: Engine + ): + generator = AdvancedChatAppGenerator() + + app_config = WorkflowUIBasedAppConfig( + tenant_id="tenant", + app_id="app", + app_mode=AppMode.ADVANCED_CHAT, + additional_features=AppAdditionalFeatures(), + variables=[], + workflow_id="workflow-id", + ) + + monkeypatch.setattr( + "core.app.apps.advanced_chat.app_generator.AdvancedChatAppConfigManager.get_app_config", + lambda app_model, workflow: app_config, + ) + monkeypatch.setattr( + "core.app.apps.advanced_chat.app_generator.FileUploadConfigManager.convert", + lambda features_dict, is_vision=False: None, + ) + DummyTraceQueueManager = type( + "_DummyTraceQueueManager", + (TraceQueueManager,), + { + "__init__": lambda self, app_id=None, user_id=None: ( + setattr(self, "app_id", app_id) or setattr(self, "user_id", user_id) + ) + }, + ) + monkeypatch.setattr( + "core.app.apps.advanced_chat.app_generator.TraceQueueManager", + DummyTraceQueueManager, + ) + monkeypatch.setattr( + "core.app.apps.advanced_chat.app_generator.DifyCoreRepositoryFactory.create_workflow_execution_repository", + lambda **kwargs: SimpleNamespace(), + ) + monkeypatch.setattr( + "core.app.apps.advanced_chat.app_generator.DifyCoreRepositoryFactory.create_workflow_node_execution_repository", + lambda **kwargs: SimpleNamespace(), + ) + monkeypatch.setattr( + "core.app.apps.advanced_chat.app_generator.db", + SimpleNamespace(engine=sqlite_engine, session=unbound_session), + ) + + captured = {} + + def _fake_generate(**kwargs): + captured.update(kwargs) + return {"ok": True} + + monkeypatch.setattr(generator, "_generate", _fake_generate) + + app_model = _make_app() + workflow = _make_workflow() + user = _make_end_user(end_user_id="end-user", session_id="session") generator.generate( app_model=app_model, @@ -1376,11 +1379,11 @@ class TestAdvancedChatAppGeneratorResume: monkeypatch.setattr(generator, "_generate", _fake_generate) result = generator.resume( - app_model=SimpleNamespace(id="app-id"), - workflow=SimpleNamespace(), - user=SimpleNamespace(id="end-user-id", session_id="session-id"), - conversation=SimpleNamespace(id="conversation-id"), - message=SimpleNamespace(id="message-id"), + app_model=_make_app(app_id="app-id"), + workflow=_make_workflow(), + user=_make_end_user(), + conversation=_make_conversation(), + message=_make_message(), session=unbound_session, application_generate_entity=application_generate_entity, workflow_execution_repository=SimpleNamespace(), @@ -1424,11 +1427,11 @@ class TestAdvancedChatAppGeneratorResume: monkeypatch.setattr(generator, "_generate", _fake_generate) result = generator.resume( - app_model=SimpleNamespace(id="app-id"), - workflow=SimpleNamespace(), - user=SimpleNamespace(id="end-user-id", session_id="session-id"), - conversation=SimpleNamespace(id="conversation-id"), - message=SimpleNamespace(id="message-id"), + app_model=_make_app(app_id="app-id"), + workflow=_make_workflow(), + user=_make_end_user(), + conversation=_make_conversation(), + message=_make_message(), session=unbound_session, application_generate_entity=application_generate_entity, workflow_execution_repository=SimpleNamespace(), diff --git a/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py b/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py index 6d90aa7e53b..c41c33487eb 100644 --- a/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py +++ b/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py @@ -4,12 +4,16 @@ from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest +from sqlalchemy import func, select +from sqlalchemy.orm import Session from core.app.apps.base_app_runner import AppRunner from core.app.entities.app_invoke_entities import InvokeFrom from graphon.file import FileTransferMethod, FileType from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from models.enums import CreatorUserRole +from models.model import MessageFile +from models.tools import ToolFile class TestBaseAppRunnerMultimodal: @@ -38,18 +42,18 @@ class TestBaseAppRunnerMultimodal: return manager @pytest.fixture - def mock_tool_file(self): - """Create a mock tool file.""" - tool_file = MagicMock() - tool_file.id = str(uuid4()) - return tool_file - - @pytest.fixture - def mock_message_file(self): - """Create a mock message file.""" - message_file = MagicMock() - message_file.id = str(uuid4()) - return message_file + def tool_file(self, mock_user_id: str, mock_tenant_id: str) -> ToolFile: + """Create a real transient tool-file model returned by the external file manager.""" + return ToolFile( + user_id=mock_user_id, + tenant_id=mock_tenant_id, + conversation_id=None, + file_key="generated/image.png", + mimetype="image/png", + original_url="http://example.com/image.png", + name="image.png", + size=68, + ) def test_handle_multimodal_image_content_with_url( self, @@ -57,8 +61,8 @@ class TestBaseAppRunnerMultimodal: mock_tenant_id, mock_message_id, mock_queue_manager, - mock_tool_file, - mock_message_file, + tool_file, + sqlite_session: Session, ): """Test handling image from URL.""" # Arrange @@ -72,48 +76,33 @@ class TestBaseAppRunnerMultimodal: with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class: # Setup mock tool file manager mock_mgr = MagicMock() - mock_mgr.create_file_by_url.return_value = mock_tool_file + mock_mgr.create_file_by_url.return_value = tool_file mock_mgr_class.return_value = mock_mgr - with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class: - # Setup mock message file - mock_msg_file_class.return_value = mock_message_file + message_file_id = AppRunner()._handle_multimodal_image_content( + session=sqlite_session, + content=content, + message_id=mock_message_id, + user_id=mock_user_id, + tenant_id=mock_tenant_id, + queue_manager=mock_queue_manager, + ) - file_session = MagicMock() - # Act - runner = MagicMock() - method = AppRunner._handle_multimodal_image_content - runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs) - - message_file_id = runner._handle_multimodal_image_content( - session=file_session, - content=content, - message_id=mock_message_id, - user_id=mock_user_id, - tenant_id=mock_tenant_id, - queue_manager=mock_queue_manager, - ) - - # Assert - mock_mgr.create_file_by_url.assert_called_once_with( - user_id=mock_user_id, - tenant_id=mock_tenant_id, - file_url=image_url, - conversation_id=None, - ) - - mock_msg_file_class.assert_called_once() - call_kwargs = mock_msg_file_class.call_args[1] - assert call_kwargs["message_id"] == mock_message_id - assert call_kwargs["type"] == FileType.IMAGE - assert call_kwargs["transfer_method"] == FileTransferMethod.TOOL_FILE - assert call_kwargs["belongs_to"] == "assistant" - assert call_kwargs["created_by"] == mock_user_id - - file_session.add.assert_called_once_with(mock_message_file) - file_session.flush.assert_called_once() - assert message_file_id == mock_message_file.id - mock_queue_manager.publish.assert_not_called() + mock_mgr.create_file_by_url.assert_called_once_with( + user_id=mock_user_id, + tenant_id=mock_tenant_id, + file_url=image_url, + conversation_id=None, + ) + message_file = sqlite_session.get(MessageFile, message_file_id) + assert message_file is not None + assert message_file.message_id == mock_message_id + assert message_file.type == FileType.IMAGE + assert message_file.transfer_method == FileTransferMethod.TOOL_FILE + assert message_file.belongs_to == "assistant" + assert message_file.created_by == mock_user_id + assert message_file.upload_file_id == tool_file.id + mock_queue_manager.publish.assert_not_called() def test_handle_multimodal_image_content_with_base64( self, @@ -121,8 +110,8 @@ class TestBaseAppRunnerMultimodal: mock_tenant_id, mock_message_id, mock_queue_manager, - mock_tool_file, - mock_message_file, + tool_file, + sqlite_session: Session, ): """Test handling image from base64 data.""" # Arrange @@ -141,41 +130,29 @@ class TestBaseAppRunnerMultimodal: with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class: # Setup mock tool file manager mock_mgr = MagicMock() - mock_mgr.create_file_by_raw.return_value = mock_tool_file + mock_mgr.create_file_by_raw.return_value = tool_file mock_mgr_class.return_value = mock_mgr - with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class: - mock_msg_file_class.return_value = mock_message_file + message_file_id = AppRunner()._handle_multimodal_image_content( + session=sqlite_session, + content=content, + message_id=mock_message_id, + user_id=mock_user_id, + tenant_id=mock_tenant_id, + queue_manager=mock_queue_manager, + ) - file_session = MagicMock() - runner = MagicMock() - method = AppRunner._handle_multimodal_image_content - runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs) - - message_file_id = runner._handle_multimodal_image_content( - session=file_session, - content=content, - message_id=mock_message_id, - user_id=mock_user_id, - tenant_id=mock_tenant_id, - queue_manager=mock_queue_manager, - ) - - mock_mgr.create_file_by_raw.assert_called_once() - call_kwargs = mock_mgr.create_file_by_raw.call_args[1] - assert call_kwargs["user_id"] == mock_user_id - assert call_kwargs["tenant_id"] == mock_tenant_id - assert call_kwargs["conversation_id"] is None - assert "file_binary" in call_kwargs - assert call_kwargs["mimetype"] == "image/png" - assert call_kwargs["filename"].startswith("generated_image") - assert call_kwargs["filename"].endswith(".png") - - mock_msg_file_class.assert_called_once() - file_session.add.assert_called_once() - file_session.flush.assert_called_once() - assert message_file_id == mock_message_file.id - mock_queue_manager.publish.assert_not_called() + mock_mgr.create_file_by_raw.assert_called_once() + call_kwargs = mock_mgr.create_file_by_raw.call_args[1] + assert call_kwargs["user_id"] == mock_user_id + assert call_kwargs["tenant_id"] == mock_tenant_id + assert call_kwargs["conversation_id"] is None + assert "file_binary" in call_kwargs + assert call_kwargs["mimetype"] == "image/png" + assert call_kwargs["filename"].startswith("generated_image") + assert call_kwargs["filename"].endswith(".png") + assert sqlite_session.get(MessageFile, message_file_id) is not None + mock_queue_manager.publish.assert_not_called() def test_handle_multimodal_image_content_with_base64_data_uri( self, @@ -183,8 +160,8 @@ class TestBaseAppRunnerMultimodal: mock_tenant_id, mock_message_id, mock_queue_manager, - mock_tool_file, - mock_message_file, + tool_file, + sqlite_session: Session, ): """Test handling image from base64 data with URI prefix.""" # Arrange @@ -201,29 +178,22 @@ class TestBaseAppRunnerMultimodal: with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class: # Setup mock tool file manager mock_mgr = MagicMock() - mock_mgr.create_file_by_raw.return_value = mock_tool_file + mock_mgr.create_file_by_raw.return_value = tool_file mock_mgr_class.return_value = mock_mgr - with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class: - mock_msg_file_class.return_value = mock_message_file + message_file_id = AppRunner()._handle_multimodal_image_content( + session=sqlite_session, + content=content, + message_id=mock_message_id, + user_id=mock_user_id, + tenant_id=mock_tenant_id, + queue_manager=mock_queue_manager, + ) - file_session = MagicMock() - runner = MagicMock() - method = AppRunner._handle_multimodal_image_content - runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs) - - runner._handle_multimodal_image_content( - session=file_session, - content=content, - message_id=mock_message_id, - user_id=mock_user_id, - tenant_id=mock_tenant_id, - queue_manager=mock_queue_manager, - ) - - mock_mgr.create_file_by_raw.assert_called_once() - call_kwargs = mock_mgr.create_file_by_raw.call_args[1] - assert "file_binary" in call_kwargs + mock_mgr.create_file_by_raw.assert_called_once() + call_kwargs = mock_mgr.create_file_by_raw.call_args[1] + assert "file_binary" in call_kwargs + assert sqlite_session.get(MessageFile, message_file_id) is not None def test_handle_multimodal_image_content_without_url_or_base64( self, @@ -231,6 +201,7 @@ class TestBaseAppRunnerMultimodal: mock_tenant_id, mock_message_id, mock_queue_manager, + sqlite_session: Session, ): """Test handling image content without URL or base64 data.""" # Arrange @@ -242,24 +213,19 @@ class TestBaseAppRunnerMultimodal: ) with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class: - with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class: - file_session = MagicMock() - runner = MagicMock() - method = AppRunner._handle_multimodal_image_content - runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs) + result = AppRunner()._handle_multimodal_image_content( + session=sqlite_session, + content=content, + message_id=mock_message_id, + user_id=mock_user_id, + tenant_id=mock_tenant_id, + queue_manager=mock_queue_manager, + ) - runner._handle_multimodal_image_content( - session=file_session, - content=content, - message_id=mock_message_id, - user_id=mock_user_id, - tenant_id=mock_tenant_id, - queue_manager=mock_queue_manager, - ) - - mock_mgr_class.assert_not_called() - mock_msg_file_class.assert_not_called() - mock_queue_manager.publish.assert_not_called() + assert result is None + assert sqlite_session.scalar(select(func.count()).select_from(MessageFile)) == 0 + mock_mgr_class.assert_not_called() + mock_queue_manager.publish.assert_not_called() def test_handle_multimodal_image_content_with_error( self, @@ -267,6 +233,7 @@ class TestBaseAppRunnerMultimodal: mock_tenant_id, mock_message_id, mock_queue_manager, + sqlite_session: Session, ): """Test handling image content when an error occurs.""" # Arrange @@ -282,23 +249,18 @@ class TestBaseAppRunnerMultimodal: mock_mgr.create_file_by_url.side_effect = Exception("Network error") mock_mgr_class.return_value = mock_mgr - with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class: - file_session = MagicMock() - runner = MagicMock() - method = AppRunner._handle_multimodal_image_content - runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs) + result = AppRunner()._handle_multimodal_image_content( + session=sqlite_session, + content=content, + message_id=mock_message_id, + user_id=mock_user_id, + tenant_id=mock_tenant_id, + queue_manager=mock_queue_manager, + ) - runner._handle_multimodal_image_content( - session=file_session, - content=content, - message_id=mock_message_id, - user_id=mock_user_id, - tenant_id=mock_tenant_id, - queue_manager=mock_queue_manager, - ) - - mock_msg_file_class.assert_not_called() - mock_queue_manager.publish.assert_not_called() + assert result is None + assert sqlite_session.scalar(select(func.count()).select_from(MessageFile)) == 0 + mock_queue_manager.publish.assert_not_called() def test_handle_multimodal_image_content_debugger_mode( self, @@ -306,8 +268,8 @@ class TestBaseAppRunnerMultimodal: mock_tenant_id, mock_message_id, mock_queue_manager, - mock_tool_file, - mock_message_file, + tool_file, + sqlite_session: Session, ): """Test that debugger mode sets correct created_by_role.""" # Arrange @@ -321,28 +283,21 @@ class TestBaseAppRunnerMultimodal: with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class: mock_mgr = MagicMock() - mock_mgr.create_file_by_url.return_value = mock_tool_file + mock_mgr.create_file_by_url.return_value = tool_file mock_mgr_class.return_value = mock_mgr - with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class: - mock_msg_file_class.return_value = mock_message_file + message_file_id = AppRunner()._handle_multimodal_image_content( + session=sqlite_session, + content=content, + message_id=mock_message_id, + user_id=mock_user_id, + tenant_id=mock_tenant_id, + queue_manager=mock_queue_manager, + ) - file_session = MagicMock() - runner = MagicMock() - method = AppRunner._handle_multimodal_image_content - runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs) - - runner._handle_multimodal_image_content( - session=file_session, - content=content, - message_id=mock_message_id, - user_id=mock_user_id, - tenant_id=mock_tenant_id, - queue_manager=mock_queue_manager, - ) - - call_kwargs = mock_msg_file_class.call_args[1] - assert call_kwargs["created_by_role"] == CreatorUserRole.ACCOUNT + message_file = sqlite_session.get(MessageFile, message_file_id) + assert message_file is not None + assert message_file.created_by_role == CreatorUserRole.ACCOUNT def test_handle_multimodal_image_content_service_api_mode( self, @@ -350,8 +305,8 @@ class TestBaseAppRunnerMultimodal: mock_tenant_id, mock_message_id, mock_queue_manager, - mock_tool_file, - mock_message_file, + tool_file, + sqlite_session: Session, ): """Test that service API mode sets correct created_by_role.""" # Arrange @@ -365,25 +320,18 @@ class TestBaseAppRunnerMultimodal: with patch("core.app.apps.base_app_runner.ToolFileManager", autospec=True) as mock_mgr_class: mock_mgr = MagicMock() - mock_mgr.create_file_by_url.return_value = mock_tool_file + mock_mgr.create_file_by_url.return_value = tool_file mock_mgr_class.return_value = mock_mgr - with patch("core.app.apps.base_app_runner.MessageFile", autospec=True) as mock_msg_file_class: - mock_msg_file_class.return_value = mock_message_file + message_file_id = AppRunner()._handle_multimodal_image_content( + session=sqlite_session, + content=content, + message_id=mock_message_id, + user_id=mock_user_id, + tenant_id=mock_tenant_id, + queue_manager=mock_queue_manager, + ) - file_session = MagicMock() - runner = MagicMock() - method = AppRunner._handle_multimodal_image_content - runner._handle_multimodal_image_content = lambda *args, **kwargs: method(runner, *args, **kwargs) - - runner._handle_multimodal_image_content( - session=file_session, - content=content, - message_id=mock_message_id, - user_id=mock_user_id, - tenant_id=mock_tenant_id, - queue_manager=mock_queue_manager, - ) - - call_kwargs = mock_msg_file_class.call_args[1] - assert call_kwargs["created_by_role"] == CreatorUserRole.END_USER + message_file = sqlite_session.get(MessageFile, message_file_id) + assert message_file is not None + assert message_file.created_by_role == CreatorUserRole.END_USER diff --git a/api/tests/unit_tests/core/mcp/server/test_streamable_http.py b/api/tests/unit_tests/core/mcp/server/test_streamable_http.py index 42a9df3538a..1878fe08413 100644 --- a/api/tests/unit_tests/core/mcp/server/test_streamable_http.py +++ b/api/tests/unit_tests/core/mcp/server/test_streamable_http.py @@ -22,6 +22,7 @@ from core.mcp.server.streamable_http import ( ) from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.model import App, AppMCPServer, AppMode, EndUser +from services.errors.app import TriggerWorkflowServiceModeUnavailableError class TestHandleMCPRequest: @@ -157,6 +158,29 @@ class TestHandleMCPRequest: # Verify AppGenerateService was called mock_app_generate.generate.assert_called_once() + @patch("core.mcp.server.streamable_http.AppGenerateService") + def test_handle_call_tool_returns_trigger_workflow_business_error(self, mock_app_generate): + mock_call_request = Mock(spec=types.CallToolRequest) + mock_call_request.params = Mock() + mock_call_request.params.arguments = {"query": "test question"} + mock_call_request.id = 123 + self.mock_request.root = mock_call_request + mock_app_generate.generate.side_effect = TriggerWorkflowServiceModeUnavailableError() + + result = handle_mcp_request( + Mock(), + self.app, + self.mock_request, + self.user_input_form, + self.mcp_server, + self.end_user, + 123, + ) + + assert isinstance(result, types.JSONRPCError) + assert result.error.code == types.INVALID_REQUEST + assert result.error.data == {"code": "trigger_workflow_service_mode_unavailable"} + @patch("core.mcp.server.streamable_http.AppGenerateService") def test_handle_call_tool_request_threads_protocol_version(self, mock_app_generate): """The negotiated version reaches handle_call_tool through the dispatcher.""" diff --git a/api/tests/unit_tests/extensions/otel/test_retrieval_tracing.py b/api/tests/unit_tests/extensions/otel/test_retrieval_tracing.py index 09f0d9dc4ff..bdd418e47e4 100644 --- a/api/tests/unit_tests/extensions/otel/test_retrieval_tracing.py +++ b/api/tests/unit_tests/extensions/otel/test_retrieval_tracing.py @@ -1,10 +1,11 @@ import threading from collections.abc import Callable -from unittest.mock import MagicMock, patch +from unittest.mock import patch from uuid import uuid4 import pytest from opentelemetry.trace import StatusCode, get_current_span, get_tracer +from sqlalchemy.orm import Session from core.rag.rerank.rerank_type import RerankMode from core.rag.retrieval.dataset_retrieval import DatasetRetrieval @@ -20,6 +21,7 @@ def _otel_enabled(config_overrides: Callable[..., None]) -> None: def test_knowledge_retrieval_creates_a_child_otel_span( memory_span_exporter, tracer_provider_with_memory_exporter, + sqlite_session: Session, ) -> None: """The retrieval entry point must be visible beneath its workflow node span.""" request = KnowledgeRetrievalRequest( @@ -38,7 +40,7 @@ def test_knowledge_retrieval_creates_a_child_otel_span( patch.object(retrieval, "_get_available_datasets", return_value=[]), get_tracer(__name__).start_as_current_span("knowledge-retrieval-node") as node_span, ): - assert retrieval.knowledge_retrieval(MagicMock(), request) == [] + assert retrieval.knowledge_retrieval(sqlite_session, request) == [] retrieval_span = next( span @@ -101,7 +103,6 @@ def test_retriever_thread_exception_sets_error_span_and_is_collected( expected_error = RuntimeError("retrieval failed") with ( - patch("core.rag.retrieval.dataset_retrieval.session_factory.create_session"), patch.object(retrieval, "_retriever", side_effect=expected_error), ): retrieval._run_retriever_thread_safely( @@ -139,7 +140,6 @@ def test_retriever_thread_exception_emits_skip_event_when_requested( dataset_id = str(uuid4()) with ( - patch("core.rag.retrieval.dataset_retrieval.session_factory.create_session"), patch.object(retrieval, "_retriever", side_effect=expected_error), get_tracer(__name__).start_as_current_span("dataset-retrieval-parent") as parent_span, ): diff --git a/api/tests/unit_tests/models/test_dataset_models.py b/api/tests/unit_tests/models/test_dataset_models.py index 5c058c42b10..4d691214d2b 100644 --- a/api/tests/unit_tests/models/test_dataset_models.py +++ b/api/tests/unit_tests/models/test_dataset_models.py @@ -1660,3 +1660,60 @@ class TestChildChunkSessionAccessors: assert child_chunk.dataset(session=sqlite_session) is None assert child_chunk.document(session=sqlite_session) is None assert child_chunk.segment(session=sqlite_session) is None + + +class TestDocumentSegmentNeighborAccessors: + """Regression coverage for ``DocumentSegment.previous_segment`` and ``next_segment`` refactored + to take a caller-provided session. + + These were ``@property`` accessors reaching for the global ``db.session`` internally; they are now + plain methods accepting a ``Session`` explicitly. + """ + + def test_accessors_resolve_neighboring_segments_via_caller_session(self, sqlite_session: Session): + dataset = _make_dataset(dataset_id=str(uuid4()), tenant_id=str(uuid4())) + document = _make_document(document_id=str(uuid4()), dataset_id=dataset.id, tenant_id=dataset.tenant_id) + segments = _make_segments(document, [0, 0, 0]) + sqlite_session.add_all([dataset, document, *segments]) + sqlite_session.flush() + + middle_segment = segments[1] + assert middle_segment.previous_segment(session=sqlite_session) is segments[0] + assert middle_segment.next_segment(session=sqlite_session) is segments[2] + + def test_accessors_return_none_when_neighbors_are_absent(self, sqlite_session: Session): + dataset = _make_dataset(dataset_id=str(uuid4()), tenant_id=str(uuid4())) + document = _make_document(document_id=str(uuid4()), dataset_id=dataset.id, tenant_id=dataset.tenant_id) + segment = _make_segments(document, [0])[0] + sqlite_session.add_all([dataset, document, segment]) + sqlite_session.flush() + + assert segment.previous_segment(session=sqlite_session) is None + assert segment.next_segment(session=sqlite_session) is None + + +class TestAppDatasetJoinSessionAccessors: + """Regression coverage for ``AppDatasetJoin.app`` refactored to take a caller-provided session. + + ``AppDatasetJoin.app`` was an ``@property`` accessor reaching for the global ``db.session`` internally; + it is now a plain method accepting a ``Session`` explicitly. + """ + + def test_app_accessor_resolves_app_via_caller_session(self, sqlite_session: Session): + app = _make_app(app_id=str(uuid4())) + join = AppDatasetJoin( + app_id=app.id, + dataset_id=str(uuid4()), + ) + sqlite_session.add_all([app, join]) + sqlite_session.flush() + + assert join.app(session=sqlite_session) is app + + def test_app_accessor_returns_none_when_app_absent(self, sqlite_session: Session): + join = AppDatasetJoin( + app_id=str(uuid4()), + dataset_id=str(uuid4()), + ) + + assert join.app(session=sqlite_session) is None diff --git a/api/tests/unit_tests/services/agent/test_agent_dsl_service.py b/api/tests/unit_tests/services/agent/test_agent_dsl_service.py index 3cf2bcd6372..7c0d5ef5332 100644 --- a/api/tests/unit_tests/services/agent/test_agent_dsl_service.py +++ b/api/tests/unit_tests/services/agent/test_agent_dsl_service.py @@ -4,10 +4,14 @@ from unittest.mock import Mock import pytest from pydantic import ValidationError +from sqlalchemy import select +from sqlalchemy.orm import Session from graphon.enums import BuiltinNodeTypes from models.agent import ( Agent, + AgentConfigDraft, + AgentConfigDraftType, AgentConfigRevision, AgentConfigRevisionOperation, AgentConfigSnapshot, @@ -191,7 +195,9 @@ def test_agent_package_rejects_null_file_id_for_available_assets(asset: dict) -> AgentPackage.model_validate(package) -def test_import_warnings_cover_runtime_setup_removed_from_package(monkeypatch: pytest.MonkeyPatch) -> None: +def test_import_warnings_cover_runtime_setup_removed_from_package( + monkeypatch: pytest.MonkeyPatch, unbound_session: Session +) -> None: soul = AgentSoulConfig.model_validate( { "tools": { @@ -211,7 +217,7 @@ def test_import_warnings_cover_runtime_setup_removed_from_package(monkeypatch: p ) monkeypatch.setattr("services.agent.dsl_service.get_tenant_knowledge_dataset_rows", Mock(return_value={})) - _, warnings = AgentDslService(Mock())._resolve_package_soul( + _, warnings = AgentDslService(unbound_session)._resolve_package_soul( tenant_id="tenant-1", package=make_portable_agent_package(_agent(), soul), package_path="agent_packages.agent_1", @@ -231,23 +237,29 @@ def test_agent_package_rejects_unknown_schema_version() -> None: AgentPackage.model_validate(package) -def test_export_agent_app_requires_backing_agent() -> None: - session = Mock() - session.scalar.return_value = None - +def test_export_agent_app_requires_backing_agent(sqlite_session: Session) -> None: with pytest.raises(ValueError, match="no active backing Agent"): - AgentDslService(session).export_agent_app(app=SimpleNamespace(tenant_id="tenant-1", id="app-1")) + AgentDslService(sqlite_session).export_agent_app(app=SimpleNamespace(tenant_id="tenant-1", id="app-1")) @pytest.mark.parametrize("use_draft", [True, False]) -def test_export_agent_app_uses_draft_or_active_snapshot(use_draft: bool) -> None: +def test_export_agent_app_uses_draft_or_active_snapshot(use_draft: bool, sqlite_session: Session) -> None: agent = _agent() + agent.app_id = "app-1" agent.active_config_snapshot_id = "snapshot-1" - draft = SimpleNamespace(config_snapshot_dict=AgentSoulConfig(config_note="draft").model_dump(mode="json")) - session = Mock() - session.scalar.side_effect = [agent, draft if use_draft else None] - session.execute.return_value = [] - service = AgentDslService(session) + sqlite_session.add(agent) + if use_draft: + sqlite_session.add( + AgentConfigDraft( + tenant_id="tenant-1", + agent_id=agent.id, + draft_type=AgentConfigDraftType.DRAFT, + draft_owner_key="", + config_snapshot=AgentSoulConfig(config_note="draft"), + ) + ) + sqlite_session.flush() + service = AgentDslService(sqlite_session) require_snapshot = Mock(return_value=_snapshot(soul=AgentSoulConfig(config_note="snapshot"))) service._require_snapshot = require_snapshot @@ -258,22 +270,26 @@ def test_export_agent_app_uses_draft_or_active_snapshot(use_draft: bool) -> None assert require_snapshot.call_count == (0 if use_draft else 1) -def test_export_workflow_packages_deduplicates_shared_agent() -> None: +def test_export_workflow_packages_deduplicates_shared_agent(sqlite_session: Session) -> None: graph = {"nodes": [_agent_node("node-1"), _agent_node("node-2")], "edges": []} bindings = [ - SimpleNamespace( + WorkflowAgentNodeBinding( + tenant_id="tenant-1", + app_id="app-1", + workflow_id="workflow-1", + workflow_version="draft", node_id=node_id, agent_id="agent-1", current_snapshot_id="snapshot-1", binding_type=WorkflowAgentBindingType.ROSTER_AGENT, - node_job_config_dict={"workflow_prompt": node_id}, + node_job_config={"workflow_prompt": node_id}, + created_by="account-1", ) for node_id in ("node-1", "node-2") ] - session = Mock() - session.scalars.return_value.all.return_value = bindings - session.execute.return_value = [] - service = AgentDslService(session) + sqlite_session.add_all(bindings) + sqlite_session.flush() + service = AgentDslService(sqlite_session) service._require_agent = Mock(return_value=_agent()) service._require_snapshot = Mock(return_value=_snapshot()) @@ -292,12 +308,9 @@ def test_export_workflow_packages_deduplicates_shared_agent() -> None: assert service._require_agent.call_count == 2 -def test_export_workflow_packages_rejects_incomplete_binding() -> None: - session = Mock() - session.scalars.return_value.all.return_value = [] - +def test_export_workflow_packages_rejects_incomplete_binding(sqlite_session: Session) -> None: with pytest.raises(ValueError, match="no complete persisted binding"): - AgentDslService(session).export_workflow_packages( + AgentDslService(sqlite_session).export_workflow_packages( workflow=SimpleNamespace(tenant_id="tenant-1", id="workflow-1", version="draft"), graph={"nodes": [_agent_node("node-1")], "edges": []}, ) @@ -328,9 +341,10 @@ def test_graph_without_package_bindings_removes_portable_fields() -> None: assert AGENT_NODE_JOB_DSL_KEY in graph["nodes"][0]["data"] -def test_import_agent_app_package_creates_config_and_unpublished_draft(monkeypatch: pytest.MonkeyPatch) -> None: - session = Mock() - service = AgentDslService(session) +def test_import_agent_app_package_creates_config_and_unpublished_draft( + monkeypatch: pytest.MonkeyPatch, sqlite_session: Session +) -> None: + service = AgentDslService(sqlite_session) soul = AgentSoulConfig(config_note="portable") warning = DslImportWarning(code="setup", path="agent.soul", message="setup required") service._resolve_package_soul = Mock(return_value=(soul, [warning])) @@ -361,11 +375,10 @@ def test_import_agent_app_package_creates_config_and_unpublished_draft(monkeypat assert agent.active_config_is_published is False assert app.name == "Portable Agent" assert app.description == "description" - assert session.add.call_count == 2 - assert session.flush.call_count == 2 + assert sqlite_session.scalar(select(AgentConfigDraft).where(AgentConfigDraft.agent_id == agent.id)) is not None -def test_import_workflow_packages_materializes_every_package_binding_as_inline() -> None: +def test_import_workflow_packages_materializes_every_package_binding_as_inline(sqlite_session: Session) -> None: package = make_portable_agent_package(_agent(), AgentSoulConfig()) graph = { "nodes": [ @@ -388,14 +401,22 @@ def test_import_workflow_packages_materializes_every_package_binding_as_inline() } for node in graph["nodes"][:3]: node["data"][AGENT_NODE_JOB_DSL_KEY] = {"workflow_prompt": node["id"]} - old_binding = SimpleNamespace( + old_binding = WorkflowAgentNodeBinding( id="old-binding", + tenant_id="tenant-1", + app_id="app-1", + workflow_id="workflow-1", + workflow_version="draft", + node_id="old-node", binding_type=WorkflowAgentBindingType.INLINE_AGENT, agent_id="old-inline-agent", + current_snapshot_id="old-snapshot", + node_job_config={}, + created_by="account-1", ) - session = Mock() - session.scalars.return_value.all.return_value = [old_binding] - service = AgentDslService(session) + sqlite_session.add(old_binding) + sqlite_session.flush() + service = AgentDslService(sqlite_session) imported_results = [ SimpleNamespace( agent=SimpleNamespace(id=f"inline-agent-{index}"), @@ -420,7 +441,7 @@ def test_import_workflow_packages_materializes_every_package_binding_as_inline() account=SimpleNamespace(id="account-1"), ) - session.delete.assert_called_once_with(old_binding) + assert sqlite_session.get(WorkflowAgentNodeBinding, "old-binding") is None assert retirement_candidates == {"old-inline-agent"} assert service._create_imported_inline_agent.call_count == 3 assert [call.kwargs["node_id"] for call in service._create_imported_inline_agent.call_args_list] == [ @@ -438,8 +459,10 @@ def test_import_workflow_packages_materializes_every_package_binding_as_inline() assert all(binding["binding_type"] == WorkflowAgentBindingType.INLINE_AGENT.value for binding in bindings) assert AGENT_NODE_JOB_DSL_KEY not in result["nodes"][0]["data"] assert json.loads(workflow.graph) == result - added_bindings = [item.args[0] for item in session.add.call_args_list] - assert all(isinstance(binding, WorkflowAgentNodeBinding) for binding in added_bindings) + added_bindings = sqlite_session.scalars( + select(WorkflowAgentNodeBinding).where(WorkflowAgentNodeBinding.workflow_id == "workflow-1") + ).all() + assert len(added_bindings) == 3 assert all(binding.binding_type == WorkflowAgentBindingType.INLINE_AGENT for binding in added_bindings) @@ -453,13 +476,13 @@ def test_import_workflow_packages_materializes_every_package_binding_as_inline() ({"binding_type": "invalid", AGENT_PACKAGE_REF_KEY: "agent_1"}, "invalid binding type"), ], ) -def test_import_workflow_packages_rejects_invalid_package_binding(binding: dict, error: str) -> None: - session = Mock() - session.scalars.return_value.all.return_value = [] +def test_import_workflow_packages_rejects_invalid_package_binding( + binding: dict, error: str, sqlite_session: Session +) -> None: package = make_portable_agent_package(_agent(), AgentSoulConfig()) with pytest.raises(ValueError, match=error): - AgentDslService(session).import_workflow_packages( + AgentDslService(sqlite_session).import_workflow_packages( workflow=SimpleNamespace(tenant_id="tenant-1", app_id="app-1", id="workflow-1", version="draft"), portable_graph={"nodes": [_agent_node("node-1", binding)], "edges": []}, raw_packages={"agent_1": package.model_dump(mode="json")}, @@ -467,9 +490,8 @@ def test_import_workflow_packages_rejects_invalid_package_binding(binding: dict, ) -def test_clone_inline_binding_copies_soul() -> None: - session = Mock() - service = AgentDslService(session) +def test_clone_inline_binding_copies_soul(unbound_session: Session) -> None: + service = AgentDslService(unbound_session) target_agent = SimpleNamespace(id="target-agent") target_snapshot = SimpleNamespace(id="target-snapshot") service._create_workflow_only_agent = Mock(return_value=(target_agent, target_snapshot)) @@ -504,7 +526,9 @@ def test_clone_inline_binding_copies_soul() -> None: assert create_kwargs["source"] == AgentSource.WORKFLOW -def test_extract_package_dependencies_covers_model_tools_and_knowledge(monkeypatch: pytest.MonkeyPatch) -> None: +def test_extract_package_dependencies_covers_model_tools_and_knowledge( + monkeypatch: pytest.MonkeyPatch, unbound_session: Session +) -> None: model_dependency = Mock(side_effect=lambda provider: f"model:{provider}") tool_dependency = Mock(side_effect=lambda provider: f"tool:{provider}") monkeypatch.setattr( @@ -551,7 +575,7 @@ def test_extract_package_dependencies_covers_model_tools_and_knowledge(monkeypat } ) - dependencies = AgentDslService(Mock()).extract_package_dependencies( + dependencies = AgentDslService(unbound_session).extract_package_dependencies( {"agent_1": make_portable_agent_package(_agent(), soul)} ) @@ -564,8 +588,8 @@ def test_extract_package_dependencies_covers_model_tools_and_knowledge(monkeypat ] -def test_create_imported_inline_agent_uses_import_provenance() -> None: - service = AgentDslService(Mock()) +def test_create_imported_inline_agent_uses_import_provenance(unbound_session: Session) -> None: + service = AgentDslService(unbound_session) soul = AgentSoulConfig(config_note="inline") warning = DslImportWarning(code="setup", path="agent", message="setup") service._resolve_package_soul = Mock(return_value=(soul, [warning])) @@ -587,9 +611,10 @@ def test_create_imported_inline_agent_uses_import_provenance() -> None: ) -def test_create_workflow_only_agent_sets_backing_app_and_snapshot(monkeypatch: pytest.MonkeyPatch) -> None: - session = Mock() - service = AgentDslService(session) +def test_create_workflow_only_agent_sets_backing_app_and_snapshot( + monkeypatch: pytest.MonkeyPatch, sqlite_session: Session +) -> None: + service = AgentDslService(sqlite_session) roster_service = Mock() roster_service.create_hidden_backing_app_for_workflow_agent.return_value = SimpleNamespace(id="backing-app") monkeypatch.setattr("services.agent.dsl_service.AgentRosterService", Mock(return_value=roster_service)) @@ -613,11 +638,12 @@ def test_create_workflow_only_agent_sets_backing_app_and_snapshot(monkeypatch: p assert agent.active_config_snapshot_id == "snapshot-1" assert agent.active_config_has_model is True assert agent.active_config_is_published is True - session.add.assert_called_once_with(agent) - assert session.flush.call_count == 2 + assert sqlite_session.get(Agent, agent.id) is agent -def test_resolve_package_soul_preserves_existing_and_marks_missing_knowledge(monkeypatch: pytest.MonkeyPatch) -> None: +def test_resolve_package_soul_preserves_existing_and_marks_missing_knowledge( + monkeypatch: pytest.MonkeyPatch, unbound_session: Session +) -> None: soul = AgentSoulConfig.model_validate( { "config_skills": [{"name": "skill", "file_kind": "tool_file", "file_id": "skill-file"}], @@ -638,18 +664,17 @@ def test_resolve_package_soul_preserves_existing_and_marks_missing_knowledge(mon }, } ) - session = Mock() get_dataset_rows = Mock(return_value={"existing": SimpleNamespace(id="existing")}) monkeypatch.setattr("services.agent.dsl_service.get_tenant_knowledge_dataset_rows", get_dataset_rows) - resolved, warnings = AgentDslService(session)._resolve_package_soul( + resolved, warnings = AgentDslService(unbound_session)._resolve_package_soul( tenant_id="tenant-1", package=make_portable_agent_package(_agent(), soul), package_path="agent_packages.agent_1", ) get_dataset_rows.assert_called_once_with( - session=session, + session=unbound_session, tenant_id="tenant-1", dataset_ids=["existing", "missing"], ) @@ -683,14 +708,18 @@ def test_resolve_package_soul_preserves_existing_and_marks_missing_knowledge(mon } -def test_create_snapshot_increments_version_and_records_revision() -> None: - session = Mock() - session.scalar.return_value = 2 - service = AgentDslService(session) +def test_create_snapshot_increments_version_and_records_revision(sqlite_session: Session) -> None: + agent = _agent() + first = _snapshot(snapshot_id="snapshot-1") + second = _snapshot(snapshot_id="snapshot-2") + second.version = 2 + sqlite_session.add_all([agent, first, second]) + sqlite_session.flush() + service = AgentDslService(sqlite_session) snapshot = service._create_snapshot( tenant_id="tenant-1", - agent=_agent(), + agent=agent, account_id="account-1", soul=AgentSoulConfig(config_note="version 3"), operation=AgentConfigRevisionOperation.IMPORT_PACKAGE, @@ -698,28 +727,32 @@ def test_create_snapshot_increments_version_and_records_revision() -> None: assert snapshot.version == 3 assert snapshot.home_snapshot_id is None - assert isinstance(session.add.call_args_list[0].args[0], AgentConfigSnapshot) - revision = session.add.call_args_list[1].args[0] - assert isinstance(revision, AgentConfigRevision) + revision = sqlite_session.scalar( + select(AgentConfigRevision).where(AgentConfigRevision.current_snapshot_id == snapshot.id) + ) + assert revision is not None assert revision.operation == AgentConfigRevisionOperation.IMPORT_PACKAGE - assert session.flush.call_count == 2 -def test_unique_roster_name_uses_first_available_suffix() -> None: - session = Mock() - session.scalars.return_value.all.return_value = ["Agent", "Agent import"] +def test_unique_roster_name_uses_first_available_suffix(sqlite_session: Session) -> None: + for index, name in enumerate(("Agent", "Agent import"), start=1): + agent = _agent() + agent.id = f"agent-{index}" + agent.name = name + sqlite_session.add(agent) + sqlite_session.flush() - result = AgentDslService(session)._unique_roster_name(tenant_id="tenant-1", requested="Agent") + result = AgentDslService(sqlite_session)._unique_roster_name(tenant_id="tenant-1", requested="Agent") assert result == "Agent import 2" -def test_require_helpers_and_graph_detection() -> None: - session = Mock() - service = AgentDslService(session) +def test_require_helpers_and_graph_detection(sqlite_session: Session) -> None: + service = AgentDslService(sqlite_session) agent = _agent() snapshot = _snapshot() - session.scalar.side_effect = [agent, None, snapshot, None] + sqlite_session.add_all([agent, snapshot]) + sqlite_session.flush() assert service._require_agent(tenant_id="tenant-1", agent_id="agent-1") is agent with pytest.raises(ValueError, match="source Agent"): @@ -733,17 +766,4 @@ def test_require_helpers_and_graph_detection() -> None: assert AgentDslService._agent_icon_type(AgentIconType.EMOJI.value) == AgentIconType.EMOJI assert AgentDslService._agent_icon_type(None) is None assert is_agent_v2_graph({"nodes": [_agent_node("agent")]}) is True - assert is_agent_v2_graph({"nodes": [{"id": "legacy-agent", "data": {"type": "agent", "version": "2"}}]}) is False assert is_agent_v2_graph({"nodes": ["invalid", {"data": {"type": "start"}}]}) is False - - -def test_export_workflow_packages_ignores_historical_agent_version_two() -> None: - session = Mock() - service = AgentDslService(session) - graph = {"nodes": [{"id": "legacy-agent", "data": {"type": "agent", "version": "2"}}]} - - portable_graph, packages = service.export_workflow_packages(workflow=Mock(), graph=graph) - - assert portable_graph == graph - assert packages == {} - session.scalars.assert_not_called() diff --git a/api/tests/unit_tests/services/agent/test_home_snapshot_service.py b/api/tests/unit_tests/services/agent/test_home_snapshot_service.py index 6cb64587818..3b2a638f692 100644 --- a/api/tests/unit_tests/services/agent/test_home_snapshot_service.py +++ b/api/tests/unit_tests/services/agent/test_home_snapshot_service.py @@ -12,6 +12,9 @@ from models.agent import ( AgentConfigDraftType, AgentConfigSnapshot, AgentHomeSnapshot, + AgentScope, + AgentSource, + AgentStatus, AgentWorkingResourceStatus, ) from models.agent_config_entities import AgentSoulConfig @@ -53,16 +56,31 @@ def test_home_snapshot_client_outlasts_the_gateway_snapshot_budget(monkeypatch: assert client._timeout == 45.0 -def test_validate_home_snapshot_binding_accepts_default_home_without_ledger_lookup() -> None: - session = MagicMock() +def _persist_agent(session: Session, *, app_id: str, backing_app_id: str | None) -> Agent: + agent = Agent( + id="agent-1", + tenant_id="tenant-1", + name="Snapshot Agent", + description="", + role="", + scope=AgentScope.ROSTER if backing_app_id is None else AgentScope.WORKFLOW_ONLY, + source=AgentSource.AGENT_APP if backing_app_id is None else AgentSource.WORKFLOW, + status=AgentStatus.ACTIVE, + app_id=app_id, + backing_app_id=backing_app_id, + ) + session.add(agent) + session.commit() + return agent + + +def test_validate_home_snapshot_binding_accepts_default_home_without_ledger_lookup(unbound_session: Session) -> None: validate_home_snapshot_binding( - session=session, + session=unbound_session, agent=Agent(id="agent-1"), home_snapshot_id=None, ) - session.scalar.assert_not_called() - @pytest.mark.parametrize( ("app_id", "backing_app_id", "expected_runtime_app_id"), @@ -73,12 +91,12 @@ def test_validate_home_snapshot_binding_accepts_default_home_without_ledger_look ) def test_build_apply_checkpoints_exact_active_binding( monkeypatch: pytest.MonkeyPatch, + sqlite_session: Session, app_id: str, backing_app_id: str | None, expected_runtime_app_id: str, ) -> None: - session = MagicMock() - session.scalar.return_value = SimpleNamespace(app_id=app_id, backing_app_id=backing_app_id) + _persist_agent(sqlite_session, app_id=app_id, backing_app_id=backing_app_id) binding = SimpleNamespace( backend_binding_ref="binding-ref-1", agent_id="agent-1", @@ -94,7 +112,7 @@ def test_build_apply_checkpoints_exact_active_binding( monkeypatch.setattr(AgentWorkspaceService, "validate_binding_generation", validate_generation) snapshot = AgentHomeSnapshotService.create_for_build_apply( - session=session, + session=sqlite_session, build_draft=_build_draft(), ) @@ -106,9 +124,8 @@ def test_build_apply_checkpoints_exact_active_binding( assert validate_generation.call_args.kwargs["base_home_snapshot_id"] == "home-old" -def test_build_apply_forwards_default_home_generation(monkeypatch: pytest.MonkeyPatch) -> None: - session = MagicMock() - session.scalar.return_value = SimpleNamespace(app_id="app-1", backing_app_id=None) +def test_build_apply_forwards_default_home_generation(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: + _persist_agent(sqlite_session, app_id="app-1", backing_app_id=None) binding = SimpleNamespace( backend_binding_ref="binding-ref-1", agent_id="agent-1", @@ -123,7 +140,7 @@ def test_build_apply_forwards_default_home_generation(monkeypatch: pytest.Monkey monkeypatch.setattr(AgentWorkspaceService, "validate_binding_generation", validate_generation) snapshot = AgentHomeSnapshotService.create_for_build_apply( - session=session, + session=sqlite_session, build_draft=_build_draft(home_snapshot_id=None), ) @@ -131,26 +148,26 @@ def test_build_apply_forwards_default_home_generation(monkeypatch: pytest.Monkey assert validate_generation.call_args.kwargs["base_home_snapshot_id"] is None -def test_build_apply_fails_fast_without_source_binding() -> None: - session = MagicMock() +def test_build_apply_fails_fast_without_source_binding(unbound_session: Session) -> None: build_draft = _build_draft() build_draft.agent_workspace_binding_id = None with pytest.raises(AgentBuildSandboxNotFoundError): AgentHomeSnapshotService.create_for_build_apply( - session=session, + session=unbound_session, build_draft=build_draft, ) -def test_home_snapshot_collection_database_failure_propagates(monkeypatch: pytest.MonkeyPatch) -> None: - context = MagicMock() - session = context.__enter__.return_value +def test_home_snapshot_collection_database_failure_propagates( + monkeypatch: pytest.MonkeyPatch, sqlite_session: Session +) -> None: error = RuntimeError("database unavailable") - session.scalar.side_effect = error + scalar = MagicMock(side_effect=error) + monkeypatch.setattr(sqlite_session, "scalar", scalar) monkeypatch.setattr( "services.agent.home_snapshot_service.session_factory.create_session", - lambda: context, + lambda: nullcontext(sqlite_session), ) with pytest.raises(RuntimeError) as exc_info: @@ -159,6 +176,7 @@ def test_home_snapshot_collection_database_failure_propagates(monkeypatch: pytes home_snapshot_id="home-1", ) + scalar.assert_called_once() assert exc_info.value is error diff --git a/api/tests/unit_tests/services/agent/test_workflow_publish_service.py b/api/tests/unit_tests/services/agent/test_workflow_publish_service.py index 74031c57844..d50f3589b68 100644 --- a/api/tests/unit_tests/services/agent/test_workflow_publish_service.py +++ b/api/tests/unit_tests/services/agent/test_workflow_publish_service.py @@ -7,15 +7,19 @@ from sqlalchemy.orm import Session from models.agent import ( Agent, + AgentConfigSnapshot, AgentScope, + AgentSource, + AgentStatus, WorkflowAgentBindingType, WorkflowAgentNodeBinding, ) +from models.agent_config_entities import AgentSoulConfig from models.enums import AppStatus from models.model import App, AppMode from models.workflow import Workflow, WorkflowType from services.agent.dsl_service import AgentDslService -from services.agent.workflow_publish_service import WorkflowAgentPublishService, _InlineAgentOwnershipError +from services.agent.workflow_publish_service import WorkflowAgentPublishService def _workflow(*, workflow_id: str = "workflow-1", version: str = Workflow.VERSION_DRAFT) -> Workflow: @@ -33,39 +37,66 @@ def _workflow(*, workflow_id: str = "workflow-1", version: str = Workflow.VERSIO ) -def test_inline_binding_from_another_node_is_cloned(monkeypatch: pytest.MonkeyPatch) -> None: - session = Mock() - draft_workflow = _workflow() - monkeypatch.setattr( - WorkflowAgentPublishService, - "_resolve_inline_agent_graph_binding", - Mock(side_effect=_InlineAgentOwnershipError("source belongs to another node")), +def _inline_agent( + *, + agent_id: str, + workflow_id: str, + node_id: str, + tenant_id: str = "tenant-1", +) -> Agent: + return Agent( + id=agent_id, + tenant_id=tenant_id, + name=f"Inline {agent_id}", + scope=AgentScope.WORKFLOW_ONLY, + source=AgentSource.WORKFLOW, + status=AgentStatus.ACTIVE, + app_id="app-1", + workflow_id=workflow_id, + workflow_node_id=node_id, ) - monkeypatch.setattr( - WorkflowAgentPublishService, - "_resolve_existing_inline_binding_agent", - Mock(return_value=None), + + +def _snapshot(*, snapshot_id: str, agent_id: str, version: int = 1) -> AgentConfigSnapshot: + return AgentConfigSnapshot( + id=snapshot_id, + tenant_id="tenant-1", + agent_id=agent_id, + version=version, + config_snapshot=AgentSoulConfig(), ) - clone = Mock(return_value=(SimpleNamespace(id="target-agent"), "target-snapshot")) - monkeypatch.setattr(WorkflowAgentPublishService, "_clone_inline_graph_binding_for_node", clone) + + +def test_inline_binding_from_another_node_is_cloned(monkeypatch: pytest.MonkeyPatch, sqlite_session: Session) -> None: + source_agent = _inline_agent(agent_id="source-agent", workflow_id="workflow-1", node_id="source-node") + source_snapshot = _snapshot(snapshot_id="source-snapshot", agent_id=source_agent.id) + sqlite_session.add_all([source_agent, source_snapshot]) + sqlite_session.commit() + target_agent = _inline_agent(agent_id="target-agent", workflow_id="workflow-1", node_id="pasted-node") + target_snapshot = _snapshot(snapshot_id="target-snapshot", agent_id=target_agent.id) + clone = Mock(return_value=(target_agent, target_snapshot)) + monkeypatch.setattr(AgentDslService, "clone_inline_binding_for_node", clone) WorkflowAgentPublishService._sync_agent_binding_for_node( - session=session, - draft_workflow=draft_workflow, + session=sqlite_session, + draft_workflow=_workflow(), node_id="pasted-node", node_data={"agent_task": "Summarize the input"}, node_binding={ "binding_type": WorkflowAgentBindingType.INLINE_AGENT.value, - "agent_id": "source-agent", - "current_snapshot_id": "source-snapshot", + "agent_id": source_agent.id, + "current_snapshot_id": source_snapshot.id, }, existing_binding=None, account_id="account-1", ) + sqlite_session.flush() clone.assert_called_once() - binding = session.add.call_args.args[0] - assert isinstance(binding, WorkflowAgentNodeBinding) + binding = sqlite_session.scalar( + select(WorkflowAgentNodeBinding).where(WorkflowAgentNodeBinding.node_id == "pasted-node") + ) + assert binding is not None assert binding.agent_id == "target-agent" assert binding.current_snapshot_id == "target-snapshot" assert binding.node_job_config.workflow_prompt == "Summarize the input" @@ -103,8 +134,9 @@ def test_draft_sync_resolves_roster_agents() -> None: assert {call.args[0].agent_id for call in session.add.call_args_list} == {"agent-a", "agent-b"} -def test_restore_replaces_bindings_and_returns_only_replaced_inline_agent() -> None: +def test_restore_replaces_bindings_and_returns_only_replaced_inline_agent(sqlite_session: Session) -> None: existing_inline = WorkflowAgentNodeBinding( + id="existing-inline", tenant_id="tenant-1", app_id="app-1", workflow_id="draft-workflow", @@ -117,6 +149,7 @@ def test_restore_replaces_bindings_and_returns_only_replaced_inline_agent() -> N created_by="account-1", ) existing_roster = WorkflowAgentNodeBinding( + id="existing-roster", tenant_id="tenant-1", app_id="app-1", workflow_id="draft-workflow", @@ -129,6 +162,7 @@ def test_restore_replaces_bindings_and_returns_only_replaced_inline_agent() -> N created_by="account-1", ) source = WorkflowAgentNodeBinding( + id="source-roster", tenant_id="tenant-1", app_id="app-1", workflow_id="published-workflow", @@ -140,30 +174,34 @@ def test_restore_replaces_bindings_and_returns_only_replaced_inline_agent() -> N node_job_config={"workflow_prompt": "Use the roster agent"}, created_by="account-1", ) - session = Mock() - session.scalars.side_effect = [ - SimpleNamespace(all=lambda: [existing_inline, existing_roster]), - SimpleNamespace(all=lambda: [source]), - ] - session.scalar.return_value = SimpleNamespace( + roster_agent = Agent( id="roster-agent", + tenant_id="tenant-1", + name="Roster Agent", scope=AgentScope.ROSTER, + source=AgentSource.ROSTER, + status=AgentStatus.ACTIVE, + app_id="roster-app", active_config_snapshot_id="published-snapshot", ) + sqlite_session.add_all([existing_inline, existing_roster, source, roster_agent]) + sqlite_session.commit() retirement_candidates = WorkflowAgentPublishService.restore_agent_node_bindings_to_draft( - session=session, + session=sqlite_session, source_workflow=_workflow(workflow_id="published-workflow", version="2026-07-13 00:00:00"), draft_workflow=_workflow(workflow_id="draft-workflow"), account_id="account-2", ) - assert {item.args[0].agent_id for item in session.delete.call_args_list} == { - "old-inline-agent", - "old-roster-agent", - } - restored = session.add.call_args.args[0] - assert isinstance(restored, WorkflowAgentNodeBinding) - assert restored.workflow_id == "draft-workflow" + assert sqlite_session.get(WorkflowAgentNodeBinding, existing_inline.id) is None + assert sqlite_session.get(WorkflowAgentNodeBinding, existing_roster.id) is None + restored = sqlite_session.scalar( + select(WorkflowAgentNodeBinding).where( + WorkflowAgentNodeBinding.workflow_id == "draft-workflow", + WorkflowAgentNodeBinding.node_id == "agent-node", + ) + ) + assert restored is not None assert restored.workflow_version == Workflow.VERSION_DRAFT assert restored.agent_id == "roster-agent" assert restored.current_snapshot_id == "published-snapshot" @@ -284,6 +322,7 @@ def test_publish_binding_copy_keeps_previous_published_owner( draft_workflow=draft_workflow, published_workflow=published_workflow, ) + sqlite_session.flush() assert result is True assert sqlite_session.get(WorkflowAgentNodeBinding, previous_inline_binding.id) is previous_inline_binding @@ -299,55 +338,50 @@ def test_publish_binding_copy_keeps_previous_published_owner( assert copied.current_snapshot_id == "draft-inline-snapshot" -def test_inline_binding_reuses_existing_node_owned_agent(monkeypatch: pytest.MonkeyPatch) -> None: - session = Mock() - draft_workflow = _workflow() +def test_inline_binding_reuses_existing_node_owned_agent(sqlite_session: Session) -> None: + existing_agent = _inline_agent(agent_id="existing-agent", workflow_id="workflow-1", node_id="pasted-node") + existing_snapshot = _snapshot(snapshot_id="existing-snapshot", agent_id=existing_agent.id) existing_binding = WorkflowAgentNodeBinding( + id="existing-binding", tenant_id="tenant-1", app_id="app-1", workflow_id="workflow-1", workflow_version=Workflow.VERSION_DRAFT, node_id="pasted-node", binding_type=WorkflowAgentBindingType.INLINE_AGENT, - agent_id="existing-agent", - current_snapshot_id="existing-snapshot", + agent_id=existing_agent.id, + current_snapshot_id=existing_snapshot.id, node_job_config={}, created_by="account-1", ) - existing_agent = SimpleNamespace(id="existing-agent") - monkeypatch.setattr( - WorkflowAgentPublishService, - "_resolve_inline_agent_graph_binding", - Mock(side_effect=_InlineAgentOwnershipError("source belongs to another node")), - ) - monkeypatch.setattr( - WorkflowAgentPublishService, - "_resolve_existing_inline_binding_agent", - Mock(return_value=existing_agent), - ) - clone = Mock() - monkeypatch.setattr(WorkflowAgentPublishService, "_clone_inline_graph_binding_for_node", clone) + sqlite_session.add_all([existing_agent, existing_snapshot, existing_binding]) + sqlite_session.commit() WorkflowAgentPublishService._sync_agent_binding_for_node( - session=session, - draft_workflow=draft_workflow, + session=sqlite_session, + draft_workflow=_workflow(), node_id="pasted-node", node_data={"agent_task": "Summarize"}, node_binding={ "binding_type": WorkflowAgentBindingType.INLINE_AGENT.value, - "agent_id": "source-agent", - "current_snapshot_id": "source-snapshot", + "agent_id": "unavailable-source-agent", + "current_snapshot_id": "unavailable-source-snapshot", }, existing_binding=existing_binding, account_id="account-1", ) + sqlite_session.flush() - assert existing_binding.agent_id == "existing-agent" - assert existing_binding.current_snapshot_id == "existing-snapshot" - clone.assert_not_called() + stored = sqlite_session.get(WorkflowAgentNodeBinding, existing_binding.id) + assert stored is not None + assert stored.agent_id == "existing-agent" + assert stored.current_snapshot_id == "existing-snapshot" + assert stored.node_job_config.workflow_prompt == "Summarize" -def test_resolve_existing_inline_binding_agent_returns_valid_agent_or_none(monkeypatch: pytest.MonkeyPatch) -> None: +def test_resolve_existing_inline_binding_agent_returns_valid_agent_or_none( + monkeypatch: pytest.MonkeyPatch, unbound_session: Session +) -> None: binding = WorkflowAgentNodeBinding( tenant_id="tenant-1", app_id="app-1", @@ -360,13 +394,13 @@ def test_resolve_existing_inline_binding_agent_returns_valid_agent_or_none(monke node_job_config={}, created_by="account-1", ) - resolved = SimpleNamespace(id="agent-1") + resolved = _inline_agent(agent_id="agent-1", workflow_id="workflow-1", node_id="node-1") resolver = Mock(return_value=resolved) monkeypatch.setattr(WorkflowAgentPublishService, "_resolve_inline_agent_graph_binding", resolver) assert ( WorkflowAgentPublishService._resolve_existing_inline_binding_agent( - session=Mock(), + session=unbound_session, draft_workflow=_workflow(), node_id="node-1", existing_binding=binding, @@ -377,7 +411,7 @@ def test_resolve_existing_inline_binding_agent_returns_valid_agent_or_none(monke resolver.side_effect = ValueError("stale") assert ( WorkflowAgentPublishService._resolve_existing_inline_binding_agent( - session=Mock(), + session=unbound_session, draft_workflow=_workflow(), node_id="node-1", existing_binding=binding, @@ -386,30 +420,42 @@ def test_resolve_existing_inline_binding_agent_returns_valid_agent_or_none(monke ) -def test_resolve_roster_binding_rejects_unpublished_agent() -> None: - session = Mock() - session.scalar.return_value = None +def test_resolve_roster_binding_rejects_unpublished_agent(sqlite_session: Session) -> None: + sqlite_session.add( + Agent( + id="decoy-agent", + tenant_id="tenant-1", + name="Decoy", + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + app_id="decoy-app", + ) + ) + sqlite_session.commit() with pytest.raises(ValueError, match="unavailable or unpublished roster agent"): WorkflowAgentPublishService._resolve_roster_agent_graph_binding( - session=session, + session=sqlite_session, draft_workflow=_workflow(), node_id="agent-node", agent_id="agent-1", ) -def test_clone_inline_graph_binding_for_node_clones_source(monkeypatch: pytest.MonkeyPatch) -> None: - session = Mock() - source_agent = SimpleNamespace(id="source-agent") - source_snapshot = SimpleNamespace(id="source-snapshot") - session.scalar.side_effect = [source_agent, source_snapshot] - target_agent = SimpleNamespace(id="target-agent") - target_snapshot = SimpleNamespace(id="target-snapshot") +def test_clone_inline_graph_binding_for_node_clones_source( + monkeypatch: pytest.MonkeyPatch, sqlite_session: Session +) -> None: + source_agent = _inline_agent(agent_id="source-agent", workflow_id="source-workflow", node_id="source-node") + source_snapshot = _snapshot(snapshot_id="source-snapshot", agent_id=source_agent.id) + sqlite_session.add_all([source_agent, source_snapshot]) + sqlite_session.commit() + target_agent = _inline_agent(agent_id="target-agent", workflow_id="workflow-1", node_id="target-node") + target_snapshot = _snapshot(snapshot_id="target-snapshot", agent_id=target_agent.id) clone = Mock(return_value=(target_agent, target_snapshot)) monkeypatch.setattr(AgentDslService, "clone_inline_binding_for_node", clone) result = WorkflowAgentPublishService._clone_inline_graph_binding_for_node( - session=session, + session=sqlite_session, draft_workflow=_workflow(), node_id="target-node", source_agent_id="source-agent", @@ -427,14 +473,17 @@ def test_clone_inline_graph_binding_for_node_clones_source(monkeypatch: pytest.M ) -@pytest.mark.parametrize("scalar_results", [[None], [SimpleNamespace(id="source-agent"), None]]) -def test_clone_inline_graph_binding_for_node_rejects_missing_source(scalar_results: list[object | None]) -> None: - session = Mock() - session.scalar.side_effect = scalar_results +@pytest.mark.parametrize("persist_source_agent", [False, True]) +def test_clone_inline_graph_binding_for_node_rejects_missing_source( + sqlite_session: Session, persist_source_agent: bool +) -> None: + if persist_source_agent: + sqlite_session.add(_inline_agent(agent_id="source-agent", workflow_id="source-workflow", node_id="source-node")) + sqlite_session.commit() with pytest.raises(ValueError, match="unavailable inline agent|missing inline agent config snapshot"): WorkflowAgentPublishService._clone_inline_graph_binding_for_node( - session=session, + session=sqlite_session, draft_workflow=_workflow(), node_id="target-node", source_agent_id="source-agent", @@ -443,37 +492,45 @@ def test_clone_inline_graph_binding_for_node_rejects_missing_source(scalar_resul ) -def test_restore_clones_inline_binding_owned_by_published_workflow(monkeypatch: pytest.MonkeyPatch) -> None: +def test_restore_clones_inline_binding_owned_by_published_workflow( + monkeypatch: pytest.MonkeyPatch, sqlite_session: Session +) -> None: + source_agent = _inline_agent(agent_id="published-agent", workflow_id="published-workflow", node_id="agent-node") + source_snapshot = _snapshot(snapshot_id="published-snapshot", agent_id=source_agent.id) source = WorkflowAgentNodeBinding( + id="published-binding", tenant_id="tenant-1", app_id="app-1", workflow_id="published-workflow", workflow_version="published", node_id="agent-node", binding_type=WorkflowAgentBindingType.INLINE_AGENT, - agent_id="published-agent", - current_snapshot_id="published-snapshot", + agent_id=source_agent.id, + current_snapshot_id=source_snapshot.id, node_job_config={"workflow_prompt": "work"}, created_by="account-1", ) - session = Mock() - session.scalars.side_effect = [SimpleNamespace(all=lambda: []), SimpleNamespace(all=lambda: [source])] - monkeypatch.setattr( - WorkflowAgentPublishService, - "_resolve_inline_agent_graph_binding", - Mock(side_effect=ValueError("owned by published workflow")), - ) - clone = Mock(return_value=(SimpleNamespace(id="draft-agent"), "draft-snapshot")) - monkeypatch.setattr(WorkflowAgentPublishService, "_clone_inline_graph_binding_for_node", clone) + sqlite_session.add_all([source_agent, source_snapshot, source]) + sqlite_session.commit() + target_agent = _inline_agent(agent_id="draft-agent", workflow_id="draft-workflow", node_id="agent-node") + target_snapshot = _snapshot(snapshot_id="draft-snapshot", agent_id=target_agent.id) + clone = Mock(return_value=(target_agent, target_snapshot)) + monkeypatch.setattr(AgentDslService, "clone_inline_binding_for_node", clone) WorkflowAgentPublishService.restore_agent_node_bindings_to_draft( - session=session, + session=sqlite_session, source_workflow=_workflow(workflow_id="published-workflow", version="published"), draft_workflow=_workflow(workflow_id="draft-workflow"), account_id="account-2", ) clone.assert_called_once() - restored = session.add.call_args.args[0] + restored = sqlite_session.scalar( + select(WorkflowAgentNodeBinding).where( + WorkflowAgentNodeBinding.workflow_id == "draft-workflow", + WorkflowAgentNodeBinding.workflow_version == Workflow.VERSION_DRAFT, + ) + ) + assert restored is not None assert restored.agent_id == "draft-agent" assert restored.current_snapshot_id == "draft-snapshot" diff --git a/api/tests/unit_tests/services/agent/test_workspace_service.py b/api/tests/unit_tests/services/agent/test_workspace_service.py index 733c09896de..2a239c6e726 100644 --- a/api/tests/unit_tests/services/agent/test_workspace_service.py +++ b/api/tests/unit_tests/services/agent/test_workspace_service.py @@ -105,11 +105,6 @@ def test_workspace_client_honors_the_configured_snapshot_timeout(monkeypatch: py assert client._timeout == 123.5 -@pytest.mark.parametrize( - "sqlite_session", - [(AgentHomeSnapshot, AgentWorkspace, AgentWorkspaceBinding)], - indirect=True, -) def test_create_binding_success_persists_new_workspace_and_binding( monkeypatch: pytest.MonkeyPatch, sqlite_session: Session ) -> None: @@ -148,11 +143,6 @@ def test_create_binding_success_persists_new_workspace_and_binding( assert request.home_snapshot_ref == "home-ref" -@pytest.mark.parametrize( - "sqlite_session", - [(AgentHomeSnapshot, AgentWorkspace, AgentWorkspaceBinding)], - indirect=True, -) def test_create_binding_without_home_snapshot_uses_backend_default( monkeypatch: pytest.MonkeyPatch, sqlite_session: Session ) -> None: @@ -176,11 +166,6 @@ def test_create_binding_without_home_snapshot_uses_backend_default( assert request.home_snapshot_ref is None -@pytest.mark.parametrize( - "sqlite_session", - [(AgentHomeSnapshot, AgentWorkspace, AgentWorkspaceBinding)], - indirect=True, -) def test_create_binding_rejects_missing_explicit_home_snapshot_before_backend_call( monkeypatch: pytest.MonkeyPatch, sqlite_session: Session ) -> None: @@ -200,11 +185,6 @@ def test_create_binding_rejects_missing_explicit_home_snapshot_before_backend_ca client.create_execution_binding_sync.assert_not_called() -@pytest.mark.parametrize( - "sqlite_session", - [(AgentHomeSnapshot, AgentWorkspace, AgentWorkspaceBinding)], - indirect=True, -) def test_create_second_binding_reuses_existing_workspace( monkeypatch: pytest.MonkeyPatch, sqlite_session: Session ) -> None: @@ -243,7 +223,6 @@ def test_create_second_binding_reuses_existing_workspace( assert request.workspace_id == workspace.id -@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True) def test_get_active_binding_resolves_exact_participant(sqlite_session: Session) -> None: conversation_workspace = _workspace(workspace_id="workspace-conversation") build_workspace = _workspace( @@ -274,7 +253,6 @@ def test_get_active_binding_resolves_exact_participant(sqlite_session: Session) assert resolved.id == conversation_binding.id -@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True) def test_get_active_binding_rejects_wrong_owner(sqlite_session: Session) -> None: build_workspace = _workspace( workspace_id="workspace-build", @@ -299,7 +277,6 @@ def test_get_active_binding_rejects_wrong_owner(sqlite_session: Session) -> None assert resolved is None -@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True) def test_retire_non_final_binding_keeps_workspace_active(sqlite_session: Session) -> None: binding = _binding() other_binding = _binding(binding_id="binding-2", agent_id="agent-2") @@ -320,7 +297,6 @@ def test_retire_non_final_binding_keeps_workspace_active(sqlite_session: Session assert other_binding.status is AgentWorkingResourceStatus.ACTIVE -@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True) def test_retire_final_binding_retires_workspace(sqlite_session: Session) -> None: binding = _binding() workspace = _workspace() @@ -335,15 +311,14 @@ def test_retire_final_binding_retires_workspace(sqlite_session: Session) -> None assert workspace.retired_at == binding.retired_at -def test_retire_workspace_retires_all_active_bindings() -> None: +def test_retire_workspace_retires_all_active_bindings(sqlite_session: Session) -> None: workspace = _workspace() bindings = [_binding(), _binding(binding_id="binding-2", agent_id="agent-2")] - session = MagicMock() - session.scalar.return_value = workspace - session.scalars.return_value.all.return_value = bindings + sqlite_session.add_all([workspace, *bindings]) + sqlite_session.flush() retired_id = AgentWorkspaceService.retire_workspace( - session=session, + session=sqlite_session, tenant_id="tenant-1", workspace_id=workspace.id, ) @@ -354,7 +329,6 @@ def test_retire_workspace_retires_all_active_bindings() -> None: assert all(binding.retired_at == workspace.retired_at for binding in bindings) -@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True) def test_retire_all_for_app_retires_only_active_workspaces_for_that_app(sqlite_session: Session) -> None: active = _workspace(workspace_id="workspace-active", owner_id="conversation-active") already_retired = _workspace( @@ -390,7 +364,6 @@ def test_retire_all_for_app_retires_only_active_workspaces_for_that_app(sqlite_s assert other_binding.status is AgentWorkingResourceStatus.ACTIVE -@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True) def test_collect_binding_without_retired_workspace_destroys_binding_only( monkeypatch: pytest.MonkeyPatch, sqlite_session: Session ) -> None: @@ -414,7 +387,6 @@ def test_collect_binding_without_retired_workspace_destroys_binding_only( assert sqlite_session.get(AgentWorkspace, workspace.id) is not None -@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True) def test_collect_workspace_destroys_workspace_then_remaining_bindings( monkeypatch: pytest.MonkeyPatch, sqlite_session: Session ) -> None: diff --git a/api/tests/unit_tests/services/test_app_generate_service.py b/api/tests/unit_tests/services/test_app_generate_service.py index b0bf1a2fd4e..646a30d5cfb 100644 --- a/api/tests/unit_tests/services/test_app_generate_service.py +++ b/api/tests/unit_tests/services/test_app_generate_service.py @@ -21,13 +21,18 @@ from unittest.mock import MagicMock import pytest from pytest_mock import MockerFixture +from sqlalchemy.orm import Session import services.app_generate_service as ags_module from core.app.entities.app_invoke_entities import InvokeFrom from enums import DeploymentEdition, QuotaType from models.model import AppMode from services.app_generate_service import AppGenerateService -from services.errors.app import WorkflowIdFormatError, WorkflowNotFoundError +from services.errors.app import ( + TriggerWorkflowServiceModeUnavailableError, + WorkflowIdFormatError, + WorkflowNotFoundError, +) # --------------------------------------------------------------------------- @@ -79,10 +84,24 @@ def _make_user() -> MagicMock: return user -def _make_workflow(*, workflow_id: str = "workflow-id", created_by: str = "owner-id") -> MagicMock: +class _RealSessionTest: + @pytest.fixture(autouse=True) + def _bind_unbound_session(self, unbound_session: Session) -> None: + self.session = unbound_session + + +def _make_workflow( + *, + workflow_id: str = "workflow-id", + created_by: str = "owner-id", + node_types: tuple[str, ...] = (), +) -> MagicMock: workflow = MagicMock() workflow.id = workflow_id workflow.created_by = created_by + workflow.walk_nodes.return_value = [ + (f"node-{index}", {"type": node_type}) for index, node_type in enumerate(node_types) + ] return workflow @@ -251,7 +270,7 @@ class TestGetMaxActiveRequests: # --------------------------------------------------------------------------- # generate – every AppMode branch # --------------------------------------------------------------------------- -class TestGenerate: +class TestGenerate(_RealSessionTest): """Tests for AppGenerateService.generate covering each mode.""" @pytest.fixture(autouse=True) @@ -280,7 +299,7 @@ class TestGenerate: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=False, - session=MagicMock(), + session=self.session, ) assert result == {"result": "ok"} gen_spy.assert_called_once() @@ -301,7 +320,7 @@ class TestGenerate: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=False, - session=MagicMock(), + session=self.session, ) assert result == {"result": "agent"} gen_spy.assert_called_once() @@ -317,7 +336,7 @@ class TestGenerate: side_effect=lambda x: x, ) app = _make_app(AppMode.CHAT, is_agent=True) - session = MagicMock() + session = self.session result = AppGenerateService.generate( app_model=app, user=_make_user(), @@ -340,7 +359,7 @@ class TestGenerate: "services.app_generate_service.AgentAppGenerator.convert_to_event_stream", side_effect=lambda x: x, ) - session = MagicMock() + session = self.session result = AppGenerateService.generate( app_model=_make_app(AppMode.AGENT), @@ -371,7 +390,7 @@ class TestGenerate: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=False, - session=MagicMock(), + session=self.session, ) assert result == {"result": "chat"} gen_spy.assert_called_once() @@ -391,7 +410,7 @@ class TestGenerate: side_effect=lambda x: x, ) - session = MagicMock() + session = self.session result = AppGenerateService.generate( app_model=_make_app(AppMode.ADVANCED_CHAT), user=_make_user(), @@ -430,7 +449,7 @@ class TestGenerate: args={"workflow_id": None, "query": "hi", "inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=True, - session=MagicMock(), + session=self.session, ) # In streaming mode it should go through retrieve_events, not generate gen_instance.retrieve_events.assert_called_once() @@ -453,7 +472,7 @@ class TestGenerate: side_effect=lambda x: x, ) - session = MagicMock() + session = self.session result = AppGenerateService.generate( app_model=_make_app(AppMode.WORKFLOW), user=_make_user(), @@ -467,6 +486,84 @@ class TestGenerate: assert call_kwargs.get("pause_state_config") is not None assert call_kwargs["pause_state_config"].state_owner_user_id == "owner-id" + @pytest.mark.parametrize( + "invoke_from", + [InvokeFrom.OPENAPI, InvokeFrom.SERVICE_API, InvokeFrom.WEB_APP], + ) + @pytest.mark.parametrize("node_type", ["trigger-plugin", "trigger-schedule", "trigger-webhook"]) + def test_trigger_workflow_rejects_manual_service_surfaces( + self, + invoke_from: InvokeFrom, + node_type: str, + mocker: MockerFixture, + ) -> None: + workflow = _make_workflow(node_types=(node_type,)) + mocker.patch.object(AppGenerateService, "_get_workflow", return_value=workflow) + generate = mocker.patch("services.app_generate_service.WorkflowAppGenerator.generate") + + with pytest.raises(TriggerWorkflowServiceModeUnavailableError): + AppGenerateService.generate( + app_model=_make_app(AppMode.WORKFLOW), + user=_make_user(), + args={"inputs": {}}, + invoke_from=invoke_from, + streaming=False, + session=MagicMock(), + ) + + generate.assert_not_called() + + def test_trigger_workflow_allows_trigger_execution(self, mocker: MockerFixture) -> None: + workflow = _make_workflow(node_types=("trigger-webhook",)) + mocker.patch.object(AppGenerateService, "_get_workflow", return_value=workflow) + generate = mocker.patch( + "services.app_generate_service.WorkflowAppGenerator.generate", + return_value={"result": "trigger"}, + ) + mocker.patch( + "services.app_generate_service.WorkflowAppGenerator.convert_to_event_stream", + side_effect=lambda value: value, + ) + + result = AppGenerateService.generate( + app_model=_make_app(AppMode.WORKFLOW), + user=_make_user(), + args={"inputs": {}}, + invoke_from=InvokeFrom.TRIGGER, + streaming=False, + session=MagicMock(), + ) + + assert result == {"result": "trigger"} + generate.assert_called_once() + + def test_specific_start_workflow_version_remains_runnable(self, mocker: MockerFixture) -> None: + workflow_id = str(uuid.uuid4()) + workflow = _make_workflow(workflow_id=workflow_id, node_types=("start",)) + get_workflow = mocker.patch.object(AppGenerateService, "_get_workflow", return_value=workflow) + mocker.patch( + "services.app_generate_service.WorkflowAppGenerator.generate", + return_value={"result": "version"}, + ) + mocker.patch( + "services.app_generate_service.WorkflowAppGenerator.convert_to_event_stream", + side_effect=lambda value: value, + ) + app = _make_app(AppMode.WORKFLOW) + session = MagicMock() + + result = AppGenerateService.generate( + app_model=app, + user=_make_user(), + args={"inputs": {}, "workflow_id": workflow_id}, + invoke_from=InvokeFrom.SERVICE_API, + streaming=False, + session=session, + ) + + assert result == {"result": "version"} + get_workflow.assert_called_once_with(app, InvokeFrom.SERVICE_API, workflow_id, session=session) + # -- WORKFLOW streaming ------------------------------------------------- def test_workflow_streaming(self, mocker: MockerFixture, config_overrides: Callable[..., None]): config_overrides(PUBSUB_REDIS_CHANNEL_TYPE="streams") @@ -492,7 +589,7 @@ class TestGenerate: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=True, - session=MagicMock(), + session=self.session, ) retrieve_spy.assert_called_once() # Dispatch is gated on subscribe; simulate the SSE layer entering the @@ -511,14 +608,14 @@ class TestGenerate: args={}, invoke_from=InvokeFrom.SERVICE_API, streaming=False, - session=MagicMock(), + session=self.session, ) # --------------------------------------------------------------------------- # generate – billing / quota # --------------------------------------------------------------------------- -class TestGenerateBilling: +class TestGenerateBilling(_RealSessionTest): @pytest.fixture(autouse=True) def _common(self, mocker: MockerFixture): mocker.patch("services.app_generate_service.RateLimit", _DummyRateLimit) @@ -549,7 +646,7 @@ class TestGenerateBilling: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=False, - session=MagicMock(), + session=self.session, ) reserve_mock.assert_called_once_with(QuotaType.WORKFLOW, "tenant-id") quota_charge.commit.assert_called_once() @@ -573,7 +670,7 @@ class TestGenerateBilling: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=False, - session=MagicMock(), + session=self.session, ) def test_exception_refunds_quota_and_exits_rate_limit( @@ -601,7 +698,7 @@ class TestGenerateBilling: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=False, - session=MagicMock(), + session=self.session, ) quota_charge.refund.assert_called_once() @@ -633,7 +730,7 @@ class TestGenerateBilling: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=False, - session=MagicMock(), + session=self.session, ) # exit is called in finally block for non-streaming assert exit_calls == ["dummy-request-id"] @@ -664,7 +761,7 @@ class TestGenerateBilling: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=False, - session=MagicMock(), + session=self.session, ) quota_charge.refund.assert_called_once() @@ -698,7 +795,7 @@ class TestGenerateBilling: args={"inputs": {}}, invoke_from=InvokeFrom.SERVICE_API, streaming=True, - session=MagicMock(), + session=self.session, ) quota_charge.refund.assert_called_once() @@ -708,14 +805,16 @@ class TestGenerateBilling: # --------------------------------------------------------------------------- # _get_workflow # --------------------------------------------------------------------------- -class TestGetWorkflow: +class TestGetWorkflow(_RealSessionTest): def test_debugger_fetches_draft(self, mocker: MockerFixture): draft_wf = _make_workflow() ws = MagicMock() ws.get_draft_workflow.return_value = draft_wf mocker.patch("services.app_generate_service.WorkflowService", return_value=ws) - result = AppGenerateService._get_workflow(_make_app(AppMode.WORKFLOW), InvokeFrom.DEBUGGER, session=MagicMock()) + result = AppGenerateService._get_workflow( + _make_app(AppMode.WORKFLOW), InvokeFrom.DEBUGGER, session=self.session + ) assert result is draft_wf ws.get_draft_workflow.assert_called_once() @@ -725,7 +824,7 @@ class TestGetWorkflow: mocker.patch("services.app_generate_service.WorkflowService", return_value=ws) with pytest.raises(ValueError, match="Workflow not initialized"): - AppGenerateService._get_workflow(_make_app(AppMode.WORKFLOW), InvokeFrom.DEBUGGER, session=MagicMock()) + AppGenerateService._get_workflow(_make_app(AppMode.WORKFLOW), InvokeFrom.DEBUGGER, session=self.session) def test_non_debugger_fetches_published(self, mocker: MockerFixture): pub_wf = _make_workflow() @@ -734,7 +833,7 @@ class TestGetWorkflow: mocker.patch("services.app_generate_service.WorkflowService", return_value=ws) result = AppGenerateService._get_workflow( - _make_app(AppMode.WORKFLOW), InvokeFrom.SERVICE_API, session=MagicMock() + _make_app(AppMode.WORKFLOW), InvokeFrom.SERVICE_API, session=self.session ) assert result is pub_wf ws.get_published_workflow.assert_called_once() @@ -745,7 +844,7 @@ class TestGetWorkflow: mocker.patch("services.app_generate_service.WorkflowService", return_value=ws) with pytest.raises(ValueError, match="Workflow not published"): - AppGenerateService._get_workflow(_make_app(AppMode.WORKFLOW), InvokeFrom.SERVICE_API, session=MagicMock()) + AppGenerateService._get_workflow(_make_app(AppMode.WORKFLOW), InvokeFrom.SERVICE_API, session=self.session) def test_specific_workflow_id_valid_uuid(self, mocker: MockerFixture): valid_uuid = str(uuid.uuid4()) @@ -758,7 +857,7 @@ class TestGetWorkflow: _make_app(AppMode.WORKFLOW), InvokeFrom.SERVICE_API, workflow_id=valid_uuid, - session=MagicMock(), + session=self.session, ) assert result is specific_wf ws.get_published_workflow_by_id.assert_called_once() @@ -772,7 +871,7 @@ class TestGetWorkflow: _make_app(AppMode.WORKFLOW), InvokeFrom.SERVICE_API, workflow_id="not-a-uuid", - session=MagicMock(), + session=self.session, ) def test_specific_workflow_id_not_found(self, mocker: MockerFixture): @@ -786,14 +885,14 @@ class TestGetWorkflow: _make_app(AppMode.WORKFLOW), InvokeFrom.SERVICE_API, workflow_id=valid_uuid, - session=MagicMock(), + session=self.session, ) # --------------------------------------------------------------------------- # generate_single_iteration # --------------------------------------------------------------------------- -class TestGenerateSingleIteration: +class TestGenerateSingleIteration(_RealSessionTest): def test_advanced_chat_mode(self, mocker: MockerFixture): workflow = _make_workflow() mocker.patch.object(AppGenerateService, "_get_workflow", return_value=workflow) @@ -806,7 +905,7 @@ class TestGenerateSingleIteration: return_value={"event": "iteration"}, ) app = _make_app(AppMode.ADVANCED_CHAT) - session = MagicMock() + session = self.session result = AppGenerateService.generate_single_iteration( app_model=app, user=_make_user(), @@ -830,7 +929,7 @@ class TestGenerateSingleIteration: return_value={"event": "wf-iteration"}, ) app = _make_app(AppMode.WORKFLOW) - session = MagicMock() + session = self.session result = AppGenerateService.generate_single_iteration( app_model=app, user=_make_user(), @@ -846,14 +945,14 @@ class TestGenerateSingleIteration: app = _make_app(AppMode.CHAT) with pytest.raises(ValueError, match="Invalid app mode"): AppGenerateService.generate_single_iteration( - app_model=app, user=_make_user(), node_id="n1", args={}, session=MagicMock() + app_model=app, user=_make_user(), node_id="n1", args={}, session=self.session ) # --------------------------------------------------------------------------- # generate_single_loop # --------------------------------------------------------------------------- -class TestGenerateSingleLoop: +class TestGenerateSingleLoop(_RealSessionTest): def test_advanced_chat_mode(self, mocker: MockerFixture): workflow = _make_workflow() mocker.patch.object(AppGenerateService, "_get_workflow", return_value=workflow) @@ -866,7 +965,7 @@ class TestGenerateSingleLoop: return_value={"event": "loop"}, ) app = _make_app(AppMode.ADVANCED_CHAT) - session = MagicMock() + session = self.session result = AppGenerateService.generate_single_loop( app_model=app, user=_make_user(), @@ -890,7 +989,7 @@ class TestGenerateSingleLoop: return_value={"event": "wf-loop"}, ) app = _make_app(AppMode.WORKFLOW) - session = MagicMock() + session = self.session result = AppGenerateService.generate_single_loop( app_model=app, user=_make_user(), @@ -906,20 +1005,20 @@ class TestGenerateSingleLoop: app = _make_app(AppMode.COMPLETION) with pytest.raises(ValueError, match="Invalid app mode"): AppGenerateService.generate_single_loop( - app_model=app, user=_make_user(), node_id="n1", args=MagicMock(), session=MagicMock() + app_model=app, user=_make_user(), node_id="n1", args=MagicMock(), session=self.session ) # --------------------------------------------------------------------------- # generate_more_like_this # --------------------------------------------------------------------------- -class TestGenerateMoreLikeThis: +class TestGenerateMoreLikeThis(_RealSessionTest): def test_delegates_to_completion_generator(self, mocker: MockerFixture): gen_spy = mocker.patch( "services.app_generate_service.CompletionAppGenerator.generate_more_like_this", return_value={"result": "similar"}, ) - session = MagicMock() + session = self.session result = AppGenerateService.generate_more_like_this( app_model=_make_app(AppMode.COMPLETION), user=_make_user(), diff --git a/api/tests/unit_tests/services/test_dataset_service_lock_not_owned.py b/api/tests/unit_tests/services/test_dataset_service_lock_not_owned.py index 50ca483b976..76dc9f584d2 100644 --- a/api/tests/unit_tests/services/test_dataset_service_lock_not_owned.py +++ b/api/tests/unit_tests/services/test_dataset_service_lock_not_owned.py @@ -1,5 +1,5 @@ import types -from unittest.mock import Mock, create_autospec +from unittest.mock import Mock import pytest from redis.exceptions import LockNotOwnedError @@ -203,19 +203,48 @@ def test_add_segment_ignores_lock_not_owned( # --------------------------------------------------------------------------- +@pytest.mark.parametrize("sqlite_session", [(Account, Tenant, Dataset, Document, DocumentSegment)], indirect=True) def test_multi_create_segment_ignores_lock_not_owned( monkeypatch: pytest.MonkeyPatch, fake_current_user, fake_lock, + sqlite_session: Session, ): # Arrange - dataset = create_autospec(Dataset, instance=True) - dataset.id = "ds-1" - dataset.tenant_id = fake_current_user.current_tenant_id - dataset.indexing_technique = IndexTechniqueType.ECONOMY # again, skip high_quality path + dataset = Dataset( + id=DATASET_ID, + tenant_id=TENANT_ID, + name="Test Dataset", + description="", + created_by=USER_ID, + indexing_technique=IndexTechniqueType.ECONOMY, + ) + document = Document( + id=DOCUMENT_ID, + tenant_id=TENANT_ID, + dataset_id=DATASET_ID, + position=1, + data_source_type="upload_file", + data_source_info="{}", + batch="batch-1", + name="Test Document", + created_from="web", + created_by=USER_ID, + word_count=0, + doc_form=IndexStructureType.QA_INDEX, + ) + sqlite_session.add_all([fake_current_user._current_tenant, fake_current_user, dataset, document]) + sqlite_session.commit() - document = create_autospec(Document, instance=True) - document.id = "doc-1" - document.dataset_id = dataset.id - document.word_count = 0 - document.doc_form = IndexStructureType.QA_INDEX + result = SegmentService.multi_create_segment( + segments=[{"content": "question", "answer": "answer", "keywords": ["key"]}], + document=document, + dataset=dataset, + session=sqlite_session, + ) + + assert result is None + assert not sqlite_session.in_transaction() + assert sqlite_session.scalar(select(func.count(DocumentSegment.id))) == 0 + sqlite_session.refresh(document) + assert document.word_count == 0 diff --git a/api/tests/unit_tests/tasks/test_resume_agent_app_task.py b/api/tests/unit_tests/tasks/test_resume_agent_app_task.py index 684ee06edf3..c9d20849af7 100644 --- a/api/tests/unit_tests/tasks/test_resume_agent_app_task.py +++ b/api/tests/unit_tests/tasks/test_resume_agent_app_task.py @@ -1,154 +1,260 @@ -"""Unit tests for the ``resume_agent_app_execution`` celery task (ENG-635). - -Every DB access (``db.session.get``) and the generator are patched at the module -level, so the task's branch logic is exercised without a database or live stack. -""" +"""Unit tests for the ``resume_agent_app_execution`` Celery task (ENG-635).""" from __future__ import annotations -from unittest.mock import MagicMock +from collections.abc import Iterator +from datetime import UTC, datetime, timedelta +from uuid import uuid4 +import pytest from pytest_mock import MockerFixture +from sqlalchemy.orm import Session, scoped_session, sessionmaker from core.app.entities.app_invoke_entities import InvokeFrom -from models.account import Account +from core.workflow.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus +from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole +from models.enums import ConversationFromSource, EndUserType +from models.enums import InvokeFrom as StoredInvokeFrom from models.human_input import HumanInputForm -from models.model import App, Conversation, EndUser +from models.model import App, AppMode, Conversation, EndUser from tasks.app_generate import resume_agent_app_task as mod MODULE = "tasks.app_generate.resume_agent_app_task" -def _form(conversation_id: str = "conv-1", app_id: str = "app-1") -> MagicMock: - return MagicMock(conversation_id=conversation_id, app_id=app_id) +@pytest.fixture +def task_session(mocker: MockerFixture, sqlite_session_factory: sessionmaker[Session]) -> Iterator[Session]: + """Bind the task's Flask-SQLAlchemy session proxy to the shared SQLite database.""" + registry = scoped_session(sqlite_session_factory) + mocker.patch.object(mod.db, "session", registry) + session = registry() + yield session + registry.remove() -def _wire_db( - mocker: MockerFixture, +def _app(*, app_id: str, tenant_id: str) -> App: + return App( + id=app_id, + tenant_id=tenant_id, + name="Agent app", + description="", + mode=AppMode.AGENT_CHAT, + icon_type=None, + icon=None, + icon_background=None, + enable_site=False, + enable_api=False, + ) + + +def _conversation( *, - form=None, - app=None, - conversation=None, - account=None, - end_user=None, -) -> MagicMock: - """Patch the module ``db`` so ``db.session.get(Model, id)`` dispatches by model.""" - table = { - HumanInputForm: form, - App: app, - Conversation: conversation, - Account: account, - EndUser: end_user, - } - db = mocker.patch(f"{MODULE}.db") - db.session.get.side_effect = lambda model, _id: table.get(model) - return db + conversation_id: str, + app_id: str, + account_id: str | None = None, + end_user_id: str | None = None, + invoke_from: StoredInvokeFrom = StoredInvokeFrom.WEB_APP, +) -> Conversation: + return Conversation( + id=conversation_id, + app_id=app_id, + mode=AppMode.AGENT_CHAT, + name="Agent conversation", + inputs={}, + invoke_from=invoke_from, + from_source=ConversationFromSource.API, + from_account_id=account_id, + from_end_user_id=end_user_id, + ) -def test_resume_happy_path_account_user_sets_tenant_and_runs(mocker: MockerFixture): - conversation = MagicMock(from_account_id="acct-1", from_end_user_id=None, invoke_from=InvokeFrom.WEB_APP) - account = MagicMock() - app = MagicMock(tenant_id="tenant-1") - db = _wire_db(mocker, form=_form(), app=app, conversation=conversation, account=account) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") - - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") - - account.set_tenant_id_with_session.assert_called_once_with("tenant-1", session=db.session.return_value) - gen.return_value.resume_after_form_submission.assert_called_once() - kwargs = gen.return_value.resume_after_form_submission.call_args.kwargs - assert kwargs["conversation_id"] == "conv-1" - assert kwargs["form_id"] == "form-1" - assert kwargs["user"] is account - assert kwargs["app_model"] is app - assert kwargs["invoke_from"] == InvokeFrom.WEB_APP - assert kwargs["session"] is db.session.return_value +def _form(*, form_id: str, conversation_id: str, app_id: str) -> HumanInputForm: + return HumanInputForm( + id=form_id, + tenant_id=str(uuid4()), + app_id=app_id, + workflow_run_id=None, + conversation_id=conversation_id, + form_kind=HumanInputFormKind.RUNTIME, + node_id="ask-human", + form_definition="{}", + rendered_content="Question", + status=HumanInputFormStatus.WAITING, + expiration_time=datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1), + ) -def test_resume_end_user_path(mocker: MockerFixture): - conversation = MagicMock(from_account_id=None, from_end_user_id="eu-1", invoke_from=InvokeFrom.WEB_APP) - end_user = MagicMock() - _wire_db(mocker, form=_form(), app=MagicMock(tenant_id="t"), conversation=conversation, end_user=end_user) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") - - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") - - assert gen.return_value.resume_after_form_submission.call_args.kwargs["user"] is end_user +def _seed_account(session: Session, *, tenant_id: str, account_id: str) -> Account: + tenant = Tenant(name="Tenant") + tenant.id = tenant_id + account = Account(name="Account", email="account@example.com") + account.id = account_id + join = TenantAccountJoin( + tenant_id=tenant_id, + account_id=account_id, + current=True, + role=TenantAccountRole.NORMAL, + ) + session.add_all([tenant, account, join]) + return account -def test_resume_preserves_debugger_invoke_from(mocker: MockerFixture): - conversation = MagicMock(from_account_id="acct-1", from_end_user_id=None, invoke_from=InvokeFrom.DEBUGGER) - account = MagicMock() - app = MagicMock(tenant_id="tenant-1") - _wire_db(mocker, form=_form(), app=app, conversation=conversation, account=account) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") +def test_resume_happy_path_account_user_sets_tenant_and_runs(mocker: MockerFixture, task_session: Session) -> None: + tenant_id, app_id, conversation_id, form_id, account_id = (str(uuid4()) for _ in range(5)) + app = _app(app_id=app_id, tenant_id=tenant_id) + account = _seed_account(task_session, tenant_id=tenant_id, account_id=account_id) + conversation = _conversation(conversation_id=conversation_id, app_id=app_id, account_id=account_id) + task_session.add_all([app, conversation, _form(form_id=form_id, conversation_id=conversation_id, app_id=app_id)]) + task_session.commit() + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") + mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id) - assert gen.return_value.resume_after_form_submission.call_args.kwargs["invoke_from"] == InvokeFrom.DEBUGGER + call = generator.return_value.resume_after_form_submission.call_args + assert call is not None + assert call.kwargs["conversation_id"] == conversation_id + assert call.kwargs["form_id"] == form_id + assert call.kwargs["user"] is account + assert call.kwargs["app_model"] is app + assert call.kwargs["invoke_from"] == InvokeFrom.WEB_APP + assert isinstance(call.kwargs["session"], Session) + assert account.current_tenant_id == tenant_id -def test_resume_returns_when_form_missing(mocker: MockerFixture): - _wire_db(mocker, form=None) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") +def test_resume_end_user_path(mocker: MockerFixture, task_session: Session) -> None: + tenant_id, app_id, conversation_id, form_id, end_user_id = (str(uuid4()) for _ in range(5)) + app = _app(app_id=app_id, tenant_id=tenant_id) + end_user = EndUser( + id=end_user_id, + tenant_id=tenant_id, + app_id=app_id, + type=EndUserType.BROWSER, + name="End user", + session_id="browser-session", + ) + task_session.add_all( + [ + app, + end_user, + _conversation(conversation_id=conversation_id, app_id=app_id, end_user_id=end_user_id), + _form(form_id=form_id, conversation_id=conversation_id, app_id=app_id), + ] + ) + task_session.commit() + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") + mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id) - gen.assert_not_called() + assert generator.return_value.resume_after_form_submission.call_args.kwargs["user"] is end_user -def test_resume_returns_on_conversation_mismatch(mocker: MockerFixture): - _wire_db(mocker, form=_form(conversation_id="other-conv")) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") +def test_resume_preserves_debugger_invoke_from(mocker: MockerFixture, task_session: Session) -> None: + tenant_id, app_id, conversation_id, form_id, account_id = (str(uuid4()) for _ in range(5)) + app = _app(app_id=app_id, tenant_id=tenant_id) + _seed_account(task_session, tenant_id=tenant_id, account_id=account_id) + task_session.add_all( + [ + app, + _conversation( + conversation_id=conversation_id, + app_id=app_id, + account_id=account_id, + invoke_from=StoredInvokeFrom.DEBUGGER, + ), + _form(form_id=form_id, conversation_id=conversation_id, app_id=app_id), + ] + ) + task_session.commit() + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") + mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id) - gen.assert_not_called() + assert generator.return_value.resume_after_form_submission.call_args.kwargs["invoke_from"] == InvokeFrom.DEBUGGER -def test_resume_returns_when_app_missing(mocker: MockerFixture): - _wire_db(mocker, form=_form(), app=None) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") - - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") - - gen.assert_not_called() +@pytest.mark.usefixtures("task_session") +def test_resume_returns_when_form_missing(mocker: MockerFixture) -> None: + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") + mod.resume_agent_app_execution(conversation_id=str(uuid4()), form_id=str(uuid4())) + generator.assert_not_called() -def test_resume_returns_when_conversation_missing(mocker: MockerFixture): - _wire_db(mocker, form=_form(), app=MagicMock(), conversation=None) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") - - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") - - gen.assert_not_called() +def test_resume_returns_on_conversation_mismatch(mocker: MockerFixture, task_session: Session) -> None: + app_id, form_id = str(uuid4()), str(uuid4()) + task_session.add(_form(form_id=form_id, conversation_id=str(uuid4()), app_id=app_id)) + task_session.commit() + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") + mod.resume_agent_app_execution(conversation_id=str(uuid4()), form_id=form_id) + generator.assert_not_called() -def test_resume_returns_when_no_user_resolvable(mocker: MockerFixture): - conversation = MagicMock(from_account_id=None, from_end_user_id=None, invoke_from=InvokeFrom.WEB_APP) - _wire_db(mocker, form=_form(), app=MagicMock(), conversation=conversation) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") - - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") - - gen.assert_not_called() +def test_resume_returns_when_app_missing(mocker: MockerFixture, task_session: Session) -> None: + conversation_id, form_id = str(uuid4()), str(uuid4()) + task_session.add(_form(form_id=form_id, conversation_id=conversation_id, app_id=str(uuid4()))) + task_session.commit() + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") + mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id) + generator.assert_not_called() -def test_resume_returns_when_account_id_set_but_account_gone(mocker: MockerFixture): - conversation = MagicMock(from_account_id="acct-x", from_end_user_id=None, invoke_from=InvokeFrom.WEB_APP) - _wire_db(mocker, form=_form(), app=MagicMock(), conversation=conversation, account=None) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") - - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") - - gen.assert_not_called() +def test_resume_returns_when_conversation_missing(mocker: MockerFixture, task_session: Session) -> None: + tenant_id, app_id, conversation_id, form_id = (str(uuid4()) for _ in range(4)) + task_session.add_all( + [ + _app(app_id=app_id, tenant_id=tenant_id), + _form(form_id=form_id, conversation_id=conversation_id, app_id=app_id), + ] + ) + task_session.commit() + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") + mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id) + generator.assert_not_called() -def test_resume_swallows_generator_exception(mocker: MockerFixture): - conversation = MagicMock(from_account_id="acct-1", from_end_user_id=None, invoke_from=InvokeFrom.WEB_APP) - _wire_db(mocker, form=_form(), app=MagicMock(tenant_id="t"), conversation=conversation, account=MagicMock()) - gen = mocker.patch(f"{MODULE}.AgentAppGenerator") - gen.return_value.resume_after_form_submission.side_effect = RuntimeError("boom") +def test_resume_returns_when_no_user_resolvable(mocker: MockerFixture, task_session: Session) -> None: + tenant_id, app_id, conversation_id, form_id = (str(uuid4()) for _ in range(4)) + task_session.add_all( + [ + _app(app_id=app_id, tenant_id=tenant_id), + _conversation(conversation_id=conversation_id, app_id=app_id), + _form(form_id=form_id, conversation_id=conversation_id, app_id=app_id), + ] + ) + task_session.commit() + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") + mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id) + generator.assert_not_called() - # The task must not propagate the failure (it is logged and the session closed). - mod.resume_agent_app_execution(conversation_id="conv-1", form_id="form-1") + +def test_resume_returns_when_account_id_set_but_account_gone(mocker: MockerFixture, task_session: Session) -> None: + tenant_id, app_id, conversation_id, form_id = (str(uuid4()) for _ in range(4)) + task_session.add_all( + [ + _app(app_id=app_id, tenant_id=tenant_id), + _conversation(conversation_id=conversation_id, app_id=app_id, account_id=str(uuid4())), + _form(form_id=form_id, conversation_id=conversation_id, app_id=app_id), + ] + ) + task_session.commit() + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") + mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id) + generator.assert_not_called() + + +def test_resume_swallows_generator_exception(mocker: MockerFixture, task_session: Session) -> None: + tenant_id, app_id, conversation_id, form_id, account_id = (str(uuid4()) for _ in range(5)) + _seed_account(task_session, tenant_id=tenant_id, account_id=account_id) + task_session.add_all( + [ + _app(app_id=app_id, tenant_id=tenant_id), + _conversation(conversation_id=conversation_id, app_id=app_id, account_id=account_id), + _form(form_id=form_id, conversation_id=conversation_id, app_id=app_id), + ] + ) + task_session.commit() + generator = mocker.patch(f"{MODULE}.AgentAppGenerator") + generator.return_value.resume_after_form_submission.side_effect = RuntimeError("boom") + + mod.resume_agent_app_execution(conversation_id=conversation_id, form_id=form_id) + + generator.return_value.resume_after_form_submission.assert_called_once() diff --git a/cli/AGENTS.md b/cli/AGENTS.md index 2b579f401ff..322325d7c5d 100644 --- a/cli/AGENTS.md +++ b/cli/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — difyctl (TypeScript CLI) -This package is the Node 22+, ESM TypeScript implementation of `difyctl`. Development also requires the Bun version pinned in `.bun-version`; command-tree generation and the `dev`, `test`, and `build` pre-scripts invoke it. Read [`ARD.md`] before adding a command or changing shared CLI infrastructure. Read `src/commands/AGENTS.md` for command-folder and registry rules. +This package is the Node 24+, ESM TypeScript implementation of `difyctl`. Development also requires the Bun version pinned in `.bun-version`; command-tree generation and the `dev`, `test`, and `build` pre-scripts invoke it. Read [`ARD.md`] before adding a command or changing shared CLI infrastructure. Read `src/commands/AGENTS.md` for command-folder and registry rules. ## Architecture Boundaries diff --git a/cli/package.json b/cli/package.json index 1c49f13137a..a2b750adb5f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -66,7 +66,7 @@ "vitest": "catalog:" }, "engines": { - "node": "^22.22.1" + "node": "^24.20.0" }, "difyctl": { "channel": "stable", diff --git a/cli/vite.config.ts b/cli/vite.config.ts index 85b0e264cbe..451c0312d63 100644 --- a/cli/vite.config.ts +++ b/cli/vite.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ sourcemap: true, treeshake: false, outDir: 'dist', - target: 'node22', + target: 'node24', define: { __DIFYCTL_VERSION__: JSON.stringify(buildInfo.version), __DIFYCTL_COMMIT__: JSON.stringify(buildInfo.commit), diff --git a/cli/vitest.e2e.config.ts b/cli/vitest.e2e.config.ts index 5ea77e18c1e..bc56c232f9c 100644 --- a/cli/vitest.e2e.config.ts +++ b/cli/vitest.e2e.config.ts @@ -43,7 +43,7 @@ export default defineConfig({ entry: ['src/index.ts'], format: ['esm'], outDir: 'dist', - target: 'node22', + target: 'node24', define: { __DIFYCTL_VERSION__: JSON.stringify(buildInfo.version), __DIFYCTL_COMMIT__: JSON.stringify(buildInfo.commit), diff --git a/dify-agent-runtime/docker/Dockerfile b/dify-agent-runtime/docker/Dockerfile index f8d38413e94..a8a25659e02 100644 --- a/dify-agent-runtime/docker/Dockerfile +++ b/dify-agent-runtime/docker/Dockerfile @@ -20,7 +20,7 @@ RUN CGO_ENABLED=0 go build -o /bin/shellctl ./cmd/shellctl && \ # ── Runtime stage ──────────────────────────────────────────────────────────── FROM python:3.12-slim-bookworm AS production -ARG NODE_VERSION=22.22.1 +ARG NODE_VERSION=24.20.0 ARG PNPM_VERSION=11.9.0 ARG UV_VERSION=0.8.9 diff --git a/dify-agent/.example.env b/dify-agent/.example.env index 1466a89d947..24d3390f7ec 100644 --- a/dify-agent/.example.env +++ b/dify-agent/.example.env @@ -4,15 +4,24 @@ # Redis # Redis connection URL for run records and per-run event streams. -DIFY_AGENT_REDIS_URL=redis://:difyai123456localhost:6379/0 +DIFY_AGENT_REDIS_URL=redis://:difyai123456@localhost:6379/0 # Prefix for Redis run-record and event-stream keys. DIFY_AGENT_REDIS_PREFIX=dify-agent # Shutdown and retention # Seconds to wait for active local runs during graceful shutdown before cancellation. DIFY_AGENT_SHUTDOWN_GRACE_SECONDS=30 -# Seconds to retain Redis run records and per-run event streams (default: 3 days). -DIFY_AGENT_RUN_RETENTION_SECONDS=259200 +# Seconds to retain Redis run records and per-run event streams after the last write (default: 2 hours). +DIFY_AGENT_RUN_RETENTION_SECONDS=7200 +# Approximate target maximum for replayable events retained in each per-run Redis Stream. +DIFY_AGENT_RUN_EVENT_STREAM_MAX_LENGTH=5000 +# Set false to publish every text delta without coalescing. +DIFY_AGENT_STREAM_TEXT_DELTA_COALESCING_ENABLED=true +# Soft debounce interval for compatible text deltas. Already-ready events may continue +# to merge past this interval; a waiting source triggers the timed flush. +DIFY_AGENT_STREAM_TEXT_DELTA_FLUSH_INTERVAL_MS=100 +# Flush a text-delta batch immediately once it reaches this many characters. +DIFY_AGENT_STREAM_TEXT_DELTA_MAX_CHARS=4096 # Plugin daemon # Base URL for the Dify plugin daemon used by local runs. @@ -24,14 +33,14 @@ DIFY_AGENT_PLUGIN_DAEMON_API_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc # Base URL for Dify API inner endpoints used by Agent Stub config and file requests. DIFY_AGENT_INNER_API_URL=http://localhost:5001 # Must match API/worker INNER_API_KEY_FOR_PLUGIN, not the generic INNER_API_KEY. -DIFY_AGENT_INNER_API_KEY= +DIFY_AGENT_INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1 # Runtime resources # Select one coherent Home Snapshot + Execution Binding backend: local, enterprise, or e2b. DIFY_AGENT_RUNTIME_BACKEND=local # Local backend: shellctl data-plane URL and optional bearer token. # Leave the endpoint empty when this server will not provide dify.runtime or resource endpoints. -DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT= +DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT=http://localhost:5004 DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN= # Enterprise resource operations currently fail fast with NotImplementedError. # These names are retained for the configured Enterprise Gateway boundary. @@ -54,12 +63,12 @@ DIFY_AGENT_SHELL_REDACT_PATTERNS= # Public Agent Stub URL reachable from shellctl-managed remote machines. # Use an HTTP(S) service root or an explicit /agent-stub API root. # Leave empty to avoid injecting DIFY_AGENT_STUB_* into shell.run jobs. -DIFY_AGENT_STUB_API_BASE_URL=http://localhost:5050/agent-stub +DIFY_AGENT_STUB_API_BASE_URL=http://host.docker.internal:5050/agent-stub # Optional bind override used only when DIFY_AGENT_STUB_API_BASE_URL uses grpc://. DIFY_AGENT_STUB_GRPC_BIND_ADDRESS= # Dify API base URL reachable from the Sandbox for the signed /files/* data plane, # including Config file and skill pulls. -DIFY_AGENT_SANDBOX_FILES_BASE_URL=http://localhost:5001 +DIFY_AGENT_SANDBOX_FILES_BASE_URL=http://host.docker.internal:5001 # Maximum Agent Stub upload size in MiB; forwarded to Dify API as a signed byte limit. DIFY_AGENT_STUB_UPLOAD_FILE_SIZE_LIMIT=50 # Shell command deadline for converting a Binding file to a ToolFile. @@ -84,6 +93,6 @@ DIFY_AGENT_OUTBOUND_HTTP_POOL_TIMEOUT=10 DIFY_AGENT_OUTBOUND_HTTP_MAX_CONNECTIONS=100 DIFY_AGENT_OUTBOUND_HTTP_MAX_KEEPALIVE_CONNECTIONS=20 DIFY_AGENT_OUTBOUND_HTTP_KEEPALIVE_EXPIRY=30 -DIFY_AGENT_LOCAL_SANDBOX_MATERIALIZED_HOME_ROOT= -DIFY_AGENT_LOCAL_SANDBOX_WORKSPACE_ROOT= -DIFY_AGENT_LOCAL_SANDBOX_HOME_SNAPSHOT_ROOT= +DIFY_AGENT_LOCAL_SANDBOX_MATERIALIZED_HOME_ROOT=/home/dify +DIFY_AGENT_LOCAL_SANDBOX_WORKSPACE_ROOT=/workspace +DIFY_AGENT_LOCAL_SANDBOX_HOME_SNAPSHOT_ROOT=/home/dify/.snapshots diff --git a/dify-agent/docs/dify-agent/guide/index.md b/dify-agent/docs/dify-agent/guide/index.md index de72c0ba286..99ff7b0ac28 100644 --- a/dify-agent/docs/dify-agent/guide/index.md +++ b/dify-agent/docs/dify-agent/guide/index.md @@ -34,7 +34,11 @@ also reads `.env` and `dify-agent/.env` when present. | `DIFY_AGENT_REDIS_URL` | `redis://localhost:6379/0` | Redis connection URL. | | `DIFY_AGENT_REDIS_PREFIX` | `dify-agent` | Prefix for Redis record and event keys. | | `DIFY_AGENT_SHUTDOWN_GRACE_SECONDS` | `30` | Seconds to wait for active local runs during graceful shutdown before cancellation. | -| `DIFY_AGENT_RUN_RETENTION_SECONDS` | `259200` | Seconds to retain Redis run records and per-run event streams; defaults to 3 days. | +| `DIFY_AGENT_RUN_RETENTION_SECONDS` | `7200` | Seconds to retain Redis run records and per-run event streams after their last write; defaults to 2 hours. | +| `DIFY_AGENT_RUN_EVENT_STREAM_MAX_LENGTH` | `5000` | Approximate target maximum for replayable events retained in each per-run Redis Stream. | +| `DIFY_AGENT_STREAM_TEXT_DELTA_COALESCING_ENABLED` | `true` | Set `false` to publish each text delta without coalescing. | +| `DIFY_AGENT_STREAM_TEXT_DELTA_FLUSH_INTERVAL_MS` | `100` | Soft debounce interval for compatible text deltas. Already-ready source events may continue to merge after this interval; a waiting source triggers the timed flush. Must be greater than zero; use `DIFY_AGENT_STREAM_TEXT_DELTA_COALESCING_ENABLED=false` to disable coalescing. | +| `DIFY_AGENT_STREAM_TEXT_DELTA_MAX_CHARS` | `4096` | Character threshold that flushes a buffered text-delta event immediately. | | `DIFY_AGENT_RUN_TIMEOUT_SECONDS` | `3600` | Wall-clock deadline in seconds for the Pydantic AI `agent.run(...)` model/tool loop. Deadline failures use `agent_run_limit_exceeded`. Its default intentionally matches `DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS`, but the settings are independently configurable. | | `DIFY_AGENT_BINDING_FILE_DOWNLOAD_COMMAND_TIMEOUT_SECONDS` | `210` | Shell command deadline for running the sandbox `dify-agent file upload --no-download-link` conversion. Keep it above the CLI's 180-second upload deadline. | | `DIFY_AGENT_API_TOKEN` | empty | Optional Bearer token required by private run, Execution Binding, Home Snapshot, and Binding file control-plane routes. Must match Dify API `AGENT_BACKEND_API_TOKEN`. | @@ -75,7 +79,10 @@ Example `.env`: DIFY_AGENT_REDIS_URL=redis://localhost:6379/0 DIFY_AGENT_REDIS_PREFIX=dify-agent-dev DIFY_AGENT_SHUTDOWN_GRACE_SECONDS=30 -DIFY_AGENT_RUN_RETENTION_SECONDS=259200 +DIFY_AGENT_RUN_RETENTION_SECONDS=7200 +DIFY_AGENT_RUN_EVENT_STREAM_MAX_LENGTH=5000 +DIFY_AGENT_STREAM_TEXT_DELTA_FLUSH_INTERVAL_MS=100 +DIFY_AGENT_STREAM_TEXT_DELTA_MAX_CHARS=4096 DIFY_AGENT_RUN_TIMEOUT_SECONDS=3600 DIFY_AGENT_API_TOKEN=replace-with-agent-backend-token DIFY_AGENT_PLUGIN_DAEMON_URL=http://localhost:5002 @@ -138,7 +145,15 @@ and lifecycle contract. Run records and event streams use the same retention. Status writes refresh the record TTL, and event writes refresh both the stream TTL and the corresponding -record TTL so active runs that keep producing events remain observable. +record TTL so active runs that keep producing events remain observable. Text +deltas for the same response part and provider metadata are coalesced within a +soft debounce/size window before being published. The debounce timer flushes +when reading the next source event would block; already-ready events may keep +merging until the character threshold or a non-compatible event is reached. +Every Redis Stream write also applies the configured approximate maximum length. A reconnect cursor older than the +retained window resumes from the oldest remaining event, so callers must treat +the terminal snapshot and application-owned history as authoritative rather +than relying on the Agent event stream as long-term history. ## Validate the E2B Compose deployment diff --git a/dify-agent/src/dify_agent/runtime/event_coalescer.py b/dify-agent/src/dify_agent/runtime/event_coalescer.py new file mode 100644 index 00000000000..a35d2c52fe3 --- /dev/null +++ b/dify-agent/src/dify_agent/runtime/event_coalescer.py @@ -0,0 +1,143 @@ +"""Bound high-frequency text deltas before they reach the shared event sink.""" + +import asyncio +from collections.abc import AsyncIterable, AsyncIterator +from contextlib import suppress +from dataclasses import replace + +from pydantic_ai.messages import AgentStreamEvent, PartDeltaEvent, TextPartDelta + + +DEFAULT_TEXT_DELTA_FLUSH_INTERVAL_SECONDS = 0.1 +DEFAULT_TEXT_DELTA_MAX_CHARS = 4096 + + +async def coalesce_agent_stream_events( + events: AsyncIterable[AgentStreamEvent], + *, + enabled: bool = True, + flush_interval_seconds: float = DEFAULT_TEXT_DELTA_FLUSH_INTERVAL_SECONDS, + max_chars: int = DEFAULT_TEXT_DELTA_MAX_CHARS, +) -> AsyncIterator[AgentStreamEvent]: + """Merge compatible text deltas with a soft debounce interval. + + One source event is read ahead while a text delta is buffered. Non-text events + flush the buffer first, preserving the public Pydantic AI event ordering. The + timer starts with the first buffered delta instead of sliding on every token. + Already-ready source events may continue to merge after the interval; the + interval triggers a flush once reading the next source event would block. + """ + if flush_interval_seconds <= 0: + raise ValueError("flush_interval_seconds must be positive") + if max_chars <= 0: + raise ValueError("max_chars must be positive") + if not enabled: + async for event in events: + yield event + return + + iterator = aiter(events) + next_event_task: asyncio.Future[AgentStreamEvent] | None = asyncio.ensure_future(anext(iterator)) + buffered: PartDeltaEvent | None = None + flush_deadline: float | None = None + loop = asyncio.get_running_loop() + + try: + while next_event_task is not None: + timeout = None if flush_deadline is None else max(0.0, flush_deadline - loop.time()) + try: + done, _pending = await asyncio.wait((next_event_task,), timeout=timeout) + except asyncio.CancelledError: + if buffered is not None: + yield buffered + raise + + if not done: + assert buffered is not None + yield buffered + buffered = None + flush_deadline = None + continue + + try: + event = next_event_task.result() + except StopAsyncIteration: + next_event_task = None + if buffered is not None: + yield buffered + return + except BaseException: + next_event_task = None + if buffered is not None: + yield buffered + raise + + next_event_task = asyncio.ensure_future(anext(iterator)) + text_delta = _text_delta(event) + if text_delta is None: + if buffered is not None: + yield buffered + buffered = None + flush_deadline = None + yield event + continue + + if buffered is not None and _can_merge(buffered, event): + buffered = _merge_text_deltas(buffered, event) + else: + if buffered is not None: + yield buffered + assert isinstance(event, PartDeltaEvent) + buffered = event + flush_deadline = loop.time() + flush_interval_seconds + + buffered_delta = _text_delta(buffered) + assert buffered_delta is not None + if len(buffered_delta.content_delta) >= max_chars: + yield buffered + buffered = None + flush_deadline = None + finally: + if next_event_task is not None and not next_event_task.done(): + next_event_task.cancel() + with suppress(asyncio.CancelledError, StopAsyncIteration): + _ = await next_event_task + + +def _text_delta(event: AgentStreamEvent) -> TextPartDelta | None: + if isinstance(event, PartDeltaEvent) and isinstance(event.delta, TextPartDelta): + return event.delta + return None + + +def _can_merge(left: PartDeltaEvent, right: AgentStreamEvent) -> bool: + right_delta = _text_delta(right) + if right_delta is None: + return False + left_delta = left.delta + assert isinstance(left_delta, TextPartDelta) + assert isinstance(right, PartDeltaEvent) + return ( + left.index == right.index + and left_delta.provider_name == right_delta.provider_name + and left_delta.provider_details == right_delta.provider_details + ) + + +def _merge_text_deltas(left: PartDeltaEvent, right: AgentStreamEvent) -> PartDeltaEvent: + left_delta = left.delta + right_delta = _text_delta(right) + assert isinstance(left_delta, TextPartDelta) + assert right_delta is not None + merged_delta = replace( + left_delta, + content_delta=left_delta.content_delta + right_delta.content_delta, + ) + return replace(left, delta=merged_delta) + + +__all__ = [ + "DEFAULT_TEXT_DELTA_FLUSH_INTERVAL_SECONDS", + "DEFAULT_TEXT_DELTA_MAX_CHARS", + "coalesce_agent_stream_events", +] diff --git a/dify-agent/src/dify_agent/runtime/run_scheduler.py b/dify-agent/src/dify_agent/runtime/run_scheduler.py index a0bf260bd25..5a57999547f 100644 --- a/dify-agent/src/dify_agent/runtime/run_scheduler.py +++ b/dify-agent/src/dify_agent/runtime/run_scheduler.py @@ -24,6 +24,10 @@ from agenton.compositor import CompositorSessionSnapshot, LayerProviderInput from dify_agent.protocol.schemas import AgentRunUsage, CancelRunRequest, CancelRunResponse, CreateRunRequest, RunStatus from dify_agent.runtime.cancellation import RunCancellationIntent from dify_agent.runtime.compositor_factory import create_default_layer_providers +from dify_agent.runtime.event_coalescer import ( + DEFAULT_TEXT_DELTA_FLUSH_INTERVAL_SECONDS, + DEFAULT_TEXT_DELTA_MAX_CHARS, +) from dify_agent.runtime.event_sink import RunEventSink, RunFinalizationResult, emit_run_failed from dify_agent.runtime.runner import DEFAULT_AGENT_RUN_TIMEOUT_SECONDS, AgentRunRunner from dify_agent.server.schemas import RunRecord @@ -106,6 +110,9 @@ class RunScheduler: store: RunStore shutdown_grace_seconds: float run_timeout_seconds: float + stream_text_delta_coalescing_enabled: bool + stream_text_delta_flush_interval_seconds: float + stream_text_delta_max_chars: int active_tasks: dict[str, asyncio.Task[None]] stopping: bool runner_factory: RunRunnerFactory | None @@ -122,12 +129,18 @@ class RunScheduler: dify_api_http_client: httpx.AsyncClient, shutdown_grace_seconds: float = 30, run_timeout_seconds: float = DEFAULT_AGENT_RUN_TIMEOUT_SECONDS, + stream_text_delta_coalescing_enabled: bool = True, + stream_text_delta_flush_interval_seconds: float = DEFAULT_TEXT_DELTA_FLUSH_INTERVAL_SECONDS, + stream_text_delta_max_chars: int = DEFAULT_TEXT_DELTA_MAX_CHARS, layer_providers: tuple[LayerProviderInput, ...] | None = None, runner_factory: RunRunnerFactory | None = None, ) -> None: self.store = store self.shutdown_grace_seconds = shutdown_grace_seconds self.run_timeout_seconds = run_timeout_seconds + self.stream_text_delta_coalescing_enabled = stream_text_delta_coalescing_enabled + self.stream_text_delta_flush_interval_seconds = stream_text_delta_flush_interval_seconds + self.stream_text_delta_max_chars = stream_text_delta_max_chars self.active_tasks = {} self.stopping = False self.plugin_daemon_http_client = plugin_daemon_http_client @@ -304,6 +317,9 @@ class RunScheduler: layer_providers=self.layer_providers, is_cancelled=is_cancelled, run_timeout_seconds=self.run_timeout_seconds, + stream_text_delta_coalescing_enabled=self.stream_text_delta_coalescing_enabled, + stream_text_delta_flush_interval_seconds=self.stream_text_delta_flush_interval_seconds, + stream_text_delta_max_chars=self.stream_text_delta_max_chars, ) def _discard_active_run(self, run_id: str) -> None: diff --git a/dify-agent/src/dify_agent/runtime/runner.py b/dify-agent/src/dify_agent/runtime/runner.py index b7dcd127df1..b20edcbb74f 100644 --- a/dify-agent/src/dify_agent/runtime/runner.py +++ b/dify-agent/src/dify_agent/runtime/runner.py @@ -7,8 +7,9 @@ policy is validated: - model runs: enter a fresh ``CompositorRun`` (or resume one from a snapshot), pass the current Dify system prompts as run-level instructions, run pydantic-ai with either the current ``run.user_prompts`` or deferred external - tool results, emit raw stream events with agent-message delta annotations, apply - request-level ``on_exit`` signals, and publish a terminal success or failure event; + tool results, emit stream events with bounded text-delta coalescing and + agent-message annotations, apply request-level ``on_exit`` signals, and publish + a terminal success or failure event; The Pydantic AI model is resolved from the active Agenton layer named by ``DIFY_AGENT_MODEL_LAYER_ID``. An optional history layer contributes stored message history only through session state. Once pydantic-ai binds and builds @@ -67,6 +68,11 @@ from dify_agent.runtime.agenton_validation import is_agenton_enter_validation_ru from dify_agent.runtime.compositor_factory import build_pydantic_ai_compositor, create_default_layer_providers from dify_agent.runtime.compaction import build_compaction_capability from dify_agent.runtime_backend import BindingLostError +from dify_agent.runtime.event_coalescer import ( + DEFAULT_TEXT_DELTA_FLUSH_INTERVAL_SECONDS, + DEFAULT_TEXT_DELTA_MAX_CHARS, + coalesce_agent_stream_events, +) from dify_agent.runtime.event_sink import ( RunEventSink, emit_pydantic_ai_event, @@ -187,6 +193,9 @@ class AgentRunRunner: dify_api_http_client: httpx.AsyncClient is_cancelled: Callable[[], bool] run_timeout_seconds: float + stream_text_delta_coalescing_enabled: bool + stream_text_delta_flush_interval_seconds: float + stream_text_delta_max_chars: int _terminal_session_snapshot: CompositorSessionSnapshot | None _terminal_usage: AgentRunUsage | None @@ -201,7 +210,14 @@ class AgentRunRunner: layer_providers: tuple[LayerProviderInput, ...] | None = None, is_cancelled: Callable[[], bool] | None = None, run_timeout_seconds: float = DEFAULT_AGENT_RUN_TIMEOUT_SECONDS, + stream_text_delta_coalescing_enabled: bool = True, + stream_text_delta_flush_interval_seconds: float = DEFAULT_TEXT_DELTA_FLUSH_INTERVAL_SECONDS, + stream_text_delta_max_chars: int = DEFAULT_TEXT_DELTA_MAX_CHARS, ) -> None: + if stream_text_delta_flush_interval_seconds <= 0: + raise ValueError("stream_text_delta_flush_interval_seconds must be positive") + if stream_text_delta_max_chars <= 0: + raise ValueError("stream_text_delta_max_chars must be positive") self.sink = sink self.request = request self.run_id = run_id @@ -210,6 +226,9 @@ class AgentRunRunner: self.layer_providers = layer_providers if layer_providers is not None else create_default_layer_providers() self.is_cancelled = is_cancelled or (lambda: False) self.run_timeout_seconds = run_timeout_seconds + self.stream_text_delta_coalescing_enabled = stream_text_delta_coalescing_enabled + self.stream_text_delta_flush_interval_seconds = stream_text_delta_flush_interval_seconds + self.stream_text_delta_max_chars = stream_text_delta_max_chars self._terminal_session_snapshot = None self._terminal_usage = None @@ -320,7 +339,13 @@ class AgentRunRunner: raise AgentRunValidationError(EMPTY_USER_PROMPTS_ERROR) async def handle_events(_ctx: object, events: AsyncIterable[AgentStreamEvent]) -> None: - async for event in events: + published_events = coalesce_agent_stream_events( + events, + enabled=self.stream_text_delta_coalescing_enabled, + flush_interval_seconds=self.stream_text_delta_flush_interval_seconds, + max_chars=self.stream_text_delta_max_chars, + ) + async for event in published_events: if self.is_cancelled(): raise asyncio.CancelledError text_delta = _extract_agent_message_delta(event) diff --git a/dify-agent/src/dify_agent/server/app.py b/dify-agent/src/dify_agent/server/app.py index 8144c66ae20..4b1d4ce3782 100644 --- a/dify-agent/src/dify_agent/server/app.py +++ b/dify-agent/src/dify_agent/server/app.py @@ -105,6 +105,7 @@ def create_app(settings: ServerSettings | None = None) -> FastAPI: redis, prefix=resolved_settings.redis_prefix, run_retention_seconds=resolved_settings.run_retention_seconds, + run_event_stream_max_length=resolved_settings.run_event_stream_max_length, ) scheduler = RunScheduler( store=store, @@ -112,6 +113,9 @@ def create_app(settings: ServerSettings | None = None) -> FastAPI: dify_api_http_client=dify_api_inner_http_client, shutdown_grace_seconds=resolved_settings.shutdown_grace_seconds, run_timeout_seconds=resolved_settings.run_timeout_seconds, + stream_text_delta_coalescing_enabled=resolved_settings.stream_text_delta_coalescing_enabled, + stream_text_delta_flush_interval_seconds=(resolved_settings.stream_text_delta_flush_interval_ms / 1000), + stream_text_delta_max_chars=resolved_settings.stream_text_delta_max_chars, layer_providers=layer_providers, ) state["store"] = store diff --git a/dify-agent/src/dify_agent/server/settings.py b/dify-agent/src/dify_agent/server/settings.py index 3d27e4439b7..80669d6bf19 100644 --- a/dify-agent/src/dify_agent/server/settings.py +++ b/dify-agent/src/dify_agent/server/settings.py @@ -21,6 +21,10 @@ from dify_agent.agent_stub.protocol.agent_stub import normalize_agent_stub_api_b from dify_agent.agent_stub.server.agent_stub_config import DifyApiAgentStubConfigRequestHandler from dify_agent.agent_stub.server.agent_stub_files import DifyApiAgentStubFileRequestHandler from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec, decode_server_secret_key +from dify_agent.runtime.event_coalescer import ( + DEFAULT_TEXT_DELTA_FLUSH_INTERVAL_SECONDS, + DEFAULT_TEXT_DELTA_MAX_CHARS, +) from dify_agent.runtime.runner import DEFAULT_AGENT_RUN_TIMEOUT_SECONDS from dify_agent.runtime_backend import RuntimeBackendProfile from dify_agent.runtime_backend.e2b import E2B_MAX_ACTIVE_TIMEOUT_SECONDS @@ -32,7 +36,8 @@ from dify_agent.runtime_backend.profile import ( create_runtime_backend_profile, ) -DEFAULT_RUN_RETENTION_SECONDS = 3 * 24 * 60 * 60 +DEFAULT_RUN_RETENTION_SECONDS = 2 * 60 * 60 +DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH = 5000 class ServerSettings(BaseSettings): @@ -42,6 +47,13 @@ class ServerSettings(BaseSettings): redis_prefix: str = "dify-agent" shutdown_grace_seconds: float = 30 run_retention_seconds: int = Field(default=DEFAULT_RUN_RETENTION_SECONDS, ge=1) + run_event_stream_max_length: int = Field(default=DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH, ge=1) + stream_text_delta_coalescing_enabled: bool = True + stream_text_delta_flush_interval_ms: int = Field( + default=int(DEFAULT_TEXT_DELTA_FLUSH_INTERVAL_SECONDS * 1000), + ge=1, + ) + stream_text_delta_max_chars: int = Field(default=DEFAULT_TEXT_DELTA_MAX_CHARS, ge=1) run_timeout_seconds: float = Field(default=DEFAULT_AGENT_RUN_TIMEOUT_SECONDS, gt=0) plugin_daemon_url: str = "http://localhost:5002" plugin_daemon_api_key: str = "" @@ -255,4 +267,4 @@ class ServerSettings(BaseSettings): ) -__all__ = ["DEFAULT_RUN_RETENTION_SECONDS", "ServerSettings"] +__all__ = ["DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH", "DEFAULT_RUN_RETENTION_SECONDS", "ServerSettings"] diff --git a/dify-agent/src/dify_agent/storage/redis_run_store.py b/dify-agent/src/dify_agent/storage/redis_run_store.py index e9f708e22de..a2a61cd9231 100644 --- a/dify-agent/src/dify_agent/storage/redis_run_store.py +++ b/dify-agent/src/dify_agent/storage/redis_run_store.py @@ -35,7 +35,7 @@ from dify_agent.runtime.event_sink import ( terminal_event_status_fields, ) from dify_agent.server.schemas import RunRecord, new_run_id -from dify_agent.server.settings import DEFAULT_RUN_RETENTION_SECONDS +from dify_agent.server.settings import DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH, DEFAULT_RUN_RETENTION_SECONDS from dify_agent.storage.redis_keys import run_cancel_intent_key, run_events_key, run_record_key _TERMINAL_RUN_EVENT_TYPES = {"run_succeeded", "run_failed", "run_cancelled"} @@ -75,7 +75,7 @@ end local ttl = tonumber(ARGV[8]) local updated_record_json = cjson.encode(record) -local event_id = redis.call("XADD", KEYS[2], "*", "payload", ARGV[7]) +local event_id = redis.call("XADD", KEYS[2], "MAXLEN", "~", ARGV[9], "*", "payload", ARGV[7]) redis.call("EXPIRE", KEYS[2], ttl) redis.call("SET", KEYS[1], updated_record_json, "EX", ttl) return {1, ARGV[1], event_id} @@ -100,7 +100,7 @@ if redis.call("EXISTS", KEYS[2]) == 1 then end local ttl = tonumber(ARGV[2]) -redis.call("XADD", KEYS[2], "*", "payload", ARGV[1]) +redis.call("XADD", KEYS[2], "MAXLEN", "1", "*", "payload", ARGV[1]) redis.call("EXPIRE", KEYS[2], ttl) redis.call("EXPIRE", KEYS[1], ttl) redis.call("EXPIRE", KEYS[3], ttl) @@ -135,7 +135,7 @@ end record.error_type = cjson.null local ttl = tonumber(ARGV[5]) -local event_id = redis.call("XADD", KEYS[3], "*", "payload", ARGV[4]) +local event_id = redis.call("XADD", KEYS[3], "MAXLEN", "~", ARGV[6], "*", "payload", ARGV[4]) redis.call("DEL", KEYS[2]) redis.call("EXPIRE", KEYS[3], ttl) redis.call("SET", KEYS[1], cjson.encode(record), "EX", ttl) @@ -147,16 +147,19 @@ class RedisRunStore(RunEventSink): """Async Redis implementation for run records and event logs. ``run_retention_seconds`` is applied to both the run record key and the - per-run Redis stream. Event writes run ``XADD`` and both TTL refreshes in one - Redis transaction so a newly created stream is not left without expiration if - the client is interrupted between commands. Event writes also refresh the - record TTL so long-running runs that keep producing events do not lose their - status record mid-run. + per-run Redis stream. Every public stream write applies an approximate + maximum length before refreshing both TTLs, so active runs cannot grow one + Redis key without bound without paying the CPU cost of exact per-entry + trimming. The transaction also ensures a newly created stream is not left + without expiration if the client is interrupted between commands. + Event writes refresh the record TTL so long-running runs that keep producing + events do not lose their status record mid-run. """ redis: Redis prefix: str run_retention_seconds: int + run_event_stream_max_length: int def __init__( self, @@ -164,12 +167,16 @@ class RedisRunStore(RunEventSink): *, prefix: str = "dify-agent", run_retention_seconds: int = DEFAULT_RUN_RETENTION_SECONDS, + run_event_stream_max_length: int = DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH, ) -> None: if run_retention_seconds <= 0: raise ValueError("run_retention_seconds must be positive") + if run_event_stream_max_length <= 0: + raise ValueError("run_event_stream_max_length must be positive") self.redis = redis self.prefix = prefix self.run_retention_seconds = run_retention_seconds + self.run_event_stream_max_length = run_event_stream_max_length async def create_run(self) -> RunRecord: """Persist a running run record without storing the create request.""" @@ -199,6 +206,8 @@ class RedisRunStore(RunEventSink): _ = pipeline.xadd( events_key, {"payload": payload}, + maxlen=self.run_event_stream_max_length, + approximate=True, ) _ = pipeline.expire(events_key, self.run_retention_seconds) _ = pipeline.expire(run_record_key(self.prefix, event.run_id), self.run_retention_seconds) @@ -226,6 +235,7 @@ class RedisRunStore(RunEventSink): error_type.value if error_type is not None else "", payload, str(self.run_retention_seconds), + str(self.run_event_stream_max_length), ), ) raw_result = await evaluation @@ -316,6 +326,7 @@ class RedisRunStore(RunEventSink): error or "", payload, str(self.run_retention_seconds), + str(self.run_event_stream_max_length), ), ) result = cast(list[object], await evaluation) @@ -385,4 +396,9 @@ def _decode_redis_text(value: object) -> str: return value.decode() if isinstance(value, bytes) else str(value) -__all__ = ["DEFAULT_RUN_RETENTION_SECONDS", "RedisRunStore", "RunNotFoundError"] +__all__ = [ + "DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH", + "DEFAULT_RUN_RETENTION_SECONDS", + "RedisRunStore", + "RunNotFoundError", +] diff --git a/dify-agent/tests/integration/dify_agent/runtime_backend/test_working_environment.py b/dify-agent/tests/integration/dify_agent/runtime_backend/test_working_environment.py index 93a67c2e816..41deb4dfa3d 100644 --- a/dify-agent/tests/integration/dify_agent/runtime_backend/test_working_environment.py +++ b/dify-agent/tests/integration/dify_agent/runtime_backend/test_working_environment.py @@ -34,6 +34,7 @@ async def _run(lease, script: str, *, cwd: str) -> str: env={"HOME": lease.layout.home_dir}, timeout=30.0, max_output_bytes=4096, + mode="stdio", ) assert result.exit_code == 0 assert result.output_complete diff --git a/dify-agent/tests/integration/dify_agent/storage/test_terminal_finalization.py b/dify-agent/tests/integration/dify_agent/storage/test_terminal_finalization.py index af3a77c687d..5bf579bee03 100644 --- a/dify-agent/tests/integration/dify_agent/storage/test_terminal_finalization.py +++ b/dify-agent/tests/integration/dify_agent/storage/test_terminal_finalization.py @@ -153,6 +153,50 @@ def test_success_and_cancel_intent_commit_exactly_one_matching_terminal(redis_ur asyncio.run(scenario()) +def test_event_stream_is_trimmed_and_keeps_the_terminal_event(redis_url: str) -> None: + async def scenario() -> None: + client = Redis.from_url(redis_url) + prefix = f"bounded-events-{uuid4().hex}" + max_length = 100 + store = RedisRunStore( + client, + prefix=prefix, + run_retention_seconds=60, + run_event_stream_max_length=max_length, + ) + run_id: str | None = None + try: + record = await store.create_run() + run_id = record.run_id + for _ in range(1000): + _ = await store.append_event(RunStartedEvent(run_id=record.run_id)) + result = await store.finalize_run( + RunSucceededEvent( + run_id=record.run_id, + data=RunSucceededEventData( + output="done", + session_snapshot=CompositorSessionSnapshot(layers=[]), + ), + ) + ) + + assert result.applied is True + stream_length = await client.xlen(run_events_key(prefix, record.run_id)) + assert max_length <= stream_length <= max_length * 2 + events = await store.get_events(record.run_id, limit=max_length * 2) + assert events.events[-1].type == "run_succeeded" + finally: + if run_id is not None: + await client.delete( + run_record_key(prefix, run_id), + run_events_key(prefix, run_id), + run_cancel_intent_key(prefix, run_id), + ) + await client.aclose() + + asyncio.run(scenario()) + + @pytest.mark.parametrize("terminal_status", ["succeeded", "failed"]) def test_terminal_first_rejects_late_cancellation(redis_url: str, terminal_status: str) -> None: async def scenario() -> None: @@ -303,6 +347,10 @@ def test_non_owner_scheduler_cancellation_stops_owner_runner(redis_url: str) -> def terminal_session_snapshot(self) -> None: return None + @property + def terminal_usage(self) -> None: + return None + async def run(self) -> None: self.started.set() try: diff --git a/dify-agent/tests/local/dify_agent/runtime/test_event_coalescer.py b/dify-agent/tests/local/dify_agent/runtime/test_event_coalescer.py new file mode 100644 index 00000000000..2ebc475e0de --- /dev/null +++ b/dify-agent/tests/local/dify_agent/runtime/test_event_coalescer.py @@ -0,0 +1,227 @@ +import asyncio +from collections.abc import AsyncIterator +from typing import Any + +import pytest +from pydantic_ai.messages import AgentStreamEvent, PartDeltaEvent, PartEndEvent, TextPart, TextPartDelta + +from dify_agent.runtime.event_coalescer import coalesce_agent_stream_events + + +def _delta(content: str, *, index: int = 0, provider_details: dict[str, Any] | None = None) -> PartDeltaEvent: + return PartDeltaEvent( + index=index, + delta=TextPartDelta(content, provider_name="test", provider_details=provider_details), + ) + + +def _part_end(content: str = "done") -> PartEndEvent: + return PartEndEvent(index=0, part=TextPart(content)) + + +async def _events(*events: PartDeltaEvent | PartEndEvent) -> AsyncIterator[PartDeltaEvent | PartEndEvent]: + for event in events: + yield event + + +async def _collect(events: AsyncIterator[AgentStreamEvent]) -> list[AgentStreamEvent]: + return [event async for event in events] + + +def _delta_content(event: AgentStreamEvent) -> str: + assert isinstance(event, PartDeltaEvent) + assert isinstance(event.delta, TextPartDelta) + return event.delta.content_delta + + +def test_coalesces_compatible_text_deltas_and_preserves_non_text_order() -> None: + async def scenario() -> list[AgentStreamEvent]: + return await _collect( + coalesce_agent_stream_events( + _events(_delta("hel"), _delta("lo"), _part_end()), + flush_interval_seconds=10, + max_chars=100, + ) + ) + + result = asyncio.run(scenario()) + + assert len(result) == 2 + assert isinstance(result[0], PartDeltaEvent) + assert isinstance(result[0].delta, TextPartDelta) + assert result[0].delta.content_delta == "hello" + assert isinstance(result[1], PartEndEvent) + + +def test_disabled_coalescing_preserves_each_source_event() -> None: + async def scenario() -> list[AgentStreamEvent]: + return await _collect( + coalesce_agent_stream_events( + _events(_delta("hel"), _delta("lo"), _part_end()), + enabled=False, + flush_interval_seconds=10, + max_chars=100, + ) + ) + + result = asyncio.run(scenario()) + + assert len(result) == 3 + assert [_delta_content(event) for event in result[:2]] == ["hel", "lo"] + assert isinstance(result[2], PartEndEvent) + + +def test_does_not_merge_different_parts_or_provider_details() -> None: + async def scenario() -> list[AgentStreamEvent]: + return await _collect( + coalesce_agent_stream_events( + _events( + _delta("a", index=0), + _delta("b", index=1), + _delta("c", index=1, provider_details={"token": 1}), + ), + flush_interval_seconds=10, + max_chars=100, + ) + ) + + result = asyncio.run(scenario()) + + assert [_delta_content(event) for event in result] == ["a", "b", "c"] + + +def test_flushes_when_character_limit_is_reached() -> None: + async def scenario() -> list[AgentStreamEvent]: + return await _collect( + coalesce_agent_stream_events( + _events(_delta("ab"), _delta("cd"), _delta("ef")), + flush_interval_seconds=10, + max_chars=4, + ) + ) + + result = asyncio.run(scenario()) + + assert [_delta_content(event) for event in result] == ["abcd", "ef"] + + +def test_high_volume_text_deltas_are_reduced_to_size_bounded_batches() -> None: + async def scenario() -> list[AgentStreamEvent]: + async def many_deltas() -> AsyncIterator[AgentStreamEvent]: + for _ in range(20_000): + yield _delta("x") + + return await _collect( + coalesce_agent_stream_events( + many_deltas(), + flush_interval_seconds=10, + max_chars=4096, + ) + ) + + result = asyncio.run(scenario()) + + assert len(result) == 5 + assert sum(len(_delta_content(event)) for event in result) == 20_000 + + +def test_flushes_on_deadline_while_source_is_idle() -> None: + async def scenario() -> tuple[PartDeltaEvent, PartEndEvent]: + release_source = asyncio.Event() + + async def delayed_events() -> AsyncIterator[PartDeltaEvent | PartEndEvent]: + yield _delta("ready") + await release_source.wait() + yield _part_end() + + events = coalesce_agent_stream_events( + delayed_events(), + flush_interval_seconds=0.01, + max_chars=100, + ) + first = await asyncio.wait_for(anext(events), timeout=0.2) + release_source.set() + second = await asyncio.wait_for(anext(events), timeout=0.2) + with pytest.raises(StopAsyncIteration): + _ = await anext(events) + assert isinstance(first, PartDeltaEvent) + assert isinstance(second, PartEndEvent) + return first, second + + first, second = asyncio.run(scenario()) + + assert isinstance(first.delta, TextPartDelta) + assert first.delta.content_delta == "ready" + assert isinstance(second.part, TextPart) + assert second.part.content == "done" + + +def test_flushes_buffer_before_propagating_source_failure() -> None: + async def scenario() -> PartDeltaEvent: + async def failing_events() -> AsyncIterator[AgentStreamEvent]: + yield _delta("partial") + raise RuntimeError("stream failed") + + events = coalesce_agent_stream_events( + failing_events(), + flush_interval_seconds=10, + max_chars=100, + ) + first = await anext(events) + with pytest.raises(RuntimeError, match="stream failed"): + _ = await anext(events) + assert isinstance(first, PartDeltaEvent) + return first + + first = asyncio.run(scenario()) + + assert isinstance(first.delta, TextPartDelta) + assert first.delta.content_delta == "partial" + + +def test_flushes_buffer_before_consumer_cancellation() -> None: + async def scenario() -> list[AgentStreamEvent]: + source_waiting = asyncio.Event() + emitted: list[AgentStreamEvent] = [] + + async def blocked_events() -> AsyncIterator[AgentStreamEvent]: + yield _delta("partial") + source_waiting.set() + await asyncio.Event().wait() + + async def consume() -> None: + async for event in coalesce_agent_stream_events( + blocked_events(), + flush_interval_seconds=10, + max_chars=100, + ): + emitted.append(event) + + task = asyncio.create_task(consume()) + await asyncio.wait_for(source_waiting.wait(), timeout=0.2) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + return emitted + + emitted = asyncio.run(scenario()) + + assert len(emitted) == 1 + assert _delta_content(emitted[0]) == "partial" + + +@pytest.mark.parametrize( + ("flush_interval_seconds", "max_chars", "message"), + [(-0.1, 100, "positive"), (0, 100, "positive"), (0.1, 0, "positive")], +) +def test_rejects_invalid_bounds(flush_interval_seconds: float, max_chars: int, message: str) -> None: + async def scenario() -> None: + events = coalesce_agent_stream_events( + _events(_delta("a")), + flush_interval_seconds=flush_interval_seconds, + max_chars=max_chars, + ) + with pytest.raises(ValueError, match=message): + _ = await anext(events) + + asyncio.run(scenario()) diff --git a/dify-agent/tests/local/dify_agent/runtime/test_run_scheduler.py b/dify-agent/tests/local/dify_agent/runtime/test_run_scheduler.py index 45e788d0c7e..7c00424f243 100644 --- a/dify-agent/tests/local/dify_agent/runtime/test_run_scheduler.py +++ b/dify-agent/tests/local/dify_agent/runtime/test_run_scheduler.py @@ -461,7 +461,7 @@ class FinalizeSuccessOnCancellationRunner(SnapshotlessRunner): assert result.applied is True -def test_default_runner_factory_passes_run_timeout_to_runner() -> None: +def test_default_runner_factory_passes_runtime_limits_to_runner() -> None: async def scenario() -> None: store = FakeStore() record = await store.create_run() @@ -471,12 +471,18 @@ def test_default_runner_factory_passes_run_timeout_to_runner() -> None: plugin_daemon_http_client=client, dify_api_http_client=client, run_timeout_seconds=17, + stream_text_delta_coalescing_enabled=False, + stream_text_delta_flush_interval_seconds=0.25, + stream_text_delta_max_chars=2048, ) runner = scheduler._default_runner_factory(record, _request(), is_cancelled=lambda: False) assert isinstance(runner, AgentRunRunner) assert runner.run_timeout_seconds == 17 + assert runner.stream_text_delta_coalescing_enabled is False + assert runner.stream_text_delta_flush_interval_seconds == 0.25 + assert runner.stream_text_delta_max_chars == 2048 asyncio.run(scenario()) diff --git a/dify-agent/tests/local/dify_agent/server/test_app.py b/dify-agent/tests/local/dify_agent/server/test_app.py index 14d49f9f3be..7fa209c8bd1 100644 --- a/dify-agent/tests/local/dify_agent/server/test_app.py +++ b/dify-agent/tests/local/dify_agent/server/test_app.py @@ -69,6 +69,9 @@ class FakeRunScheduler: store: object shutdown_grace_seconds: float run_timeout_seconds: float + stream_text_delta_coalescing_enabled: bool + stream_text_delta_flush_interval_seconds: float + stream_text_delta_max_chars: int layer_providers: tuple[DifyAgentLayerProvider, ...] plugin_daemon_http_client: FakePluginDaemonHttpClient dify_api_http_client: FakePluginDaemonHttpClient @@ -82,11 +85,17 @@ class FakeRunScheduler: dify_api_http_client: FakePluginDaemonHttpClient, shutdown_grace_seconds: float, run_timeout_seconds: float, + stream_text_delta_coalescing_enabled: bool, + stream_text_delta_flush_interval_seconds: float, + stream_text_delta_max_chars: int, layer_providers: tuple[DifyAgentLayerProvider, ...], ) -> None: self.store = store self.shutdown_grace_seconds = shutdown_grace_seconds self.run_timeout_seconds = run_timeout_seconds + self.stream_text_delta_coalescing_enabled = stream_text_delta_coalescing_enabled + self.stream_text_delta_flush_interval_seconds = stream_text_delta_flush_interval_seconds + self.stream_text_delta_max_chars = stream_text_delta_max_chars self.layer_providers = layer_providers self.plugin_daemon_http_client = plugin_daemon_http_client self.dify_api_http_client = dify_api_http_client @@ -203,6 +212,10 @@ def test_create_app_creates_scheduler_and_closes_after_shutdown(monkeypatch: pyt shutdown_grace_seconds=5, run_timeout_seconds=17, run_retention_seconds=7, + run_event_stream_max_length=23, + stream_text_delta_coalescing_enabled=False, + stream_text_delta_flush_interval_ms=250, + stream_text_delta_max_chars=2048, plugin_daemon_url="http://plugin-daemon", plugin_daemon_api_key="daemon-secret", inner_api_url="http://dify-api", @@ -226,6 +239,9 @@ def test_create_app_creates_scheduler_and_closes_after_shutdown(monkeypatch: pyt scheduler = FakeRunScheduler.created[0] assert scheduler.shutdown_grace_seconds == 5 assert scheduler.run_timeout_seconds == 17 + assert scheduler.stream_text_delta_coalescing_enabled is False + assert scheduler.stream_text_delta_flush_interval_seconds == 0.25 + assert scheduler.stream_text_delta_max_chars == 2048 layer_providers = scheduler.layer_providers assert isinstance(layer_providers, tuple) execution_context_provider = next( @@ -284,6 +300,7 @@ def test_create_app_creates_scheduler_and_closes_after_shutdown(monkeypatch: pyt store = scheduler.store assert isinstance(store, RedisRunStore) assert store.run_retention_seconds == 7 + assert store.run_event_stream_max_length == 23 assert any(getattr(route, "path", None) == "/agent-stub/connections" for route in create_app(settings).routes) assert any( getattr(route, "path", None) == "/agent-stub/files/upload-request" for route in create_app(settings).routes diff --git a/dify-agent/tests/local/dify_agent/server/test_settings.py b/dify-agent/tests/local/dify_agent/server/test_settings.py index b94dc033122..465c6a3b304 100644 --- a/dify-agent/tests/local/dify_agent/server/test_settings.py +++ b/dify-agent/tests/local/dify_agent/server/test_settings.py @@ -8,7 +8,11 @@ from pydantic import ValidationError from dify_agent.agent_stub.server.agent_stub_files import DifyApiAgentStubFileRequestHandler from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec -from dify_agent.server.settings import ServerSettings +from dify_agent.server.settings import ( + DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH, + DEFAULT_RUN_RETENTION_SECONDS, + ServerSettings, +) from dify_agent.runtime.runner import DEFAULT_AGENT_RUN_TIMEOUT_SECONDS from dify_agent.runtime_backend.e2b import E2B_MAX_ACTIVE_TIMEOUT_SECONDS, E2BExecutionBindingBackend from dify_agent.runtime_backend.enterprise import EnterpriseExecutionBindingBackend, EnterpriseHomeSnapshotBackend @@ -79,6 +83,53 @@ def test_server_settings_rejects_non_positive_run_timeout() -> None: _ = ServerSettings(run_timeout_seconds=0) +def test_server_settings_defaults_to_bounded_short_lived_run_events( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.delenv("DIFY_AGENT_RUN_RETENTION_SECONDS", raising=False) + monkeypatch.delenv("DIFY_AGENT_RUN_EVENT_STREAM_MAX_LENGTH", raising=False) + monkeypatch.delenv("DIFY_AGENT_STREAM_TEXT_DELTA_COALESCING_ENABLED", raising=False) + monkeypatch.delenv("DIFY_AGENT_STREAM_TEXT_DELTA_FLUSH_INTERVAL_MS", raising=False) + monkeypatch.delenv("DIFY_AGENT_STREAM_TEXT_DELTA_MAX_CHARS", raising=False) + monkeypatch.chdir(tmp_path) + + settings = ServerSettings() + + assert settings.run_retention_seconds == DEFAULT_RUN_RETENTION_SECONDS == 7200 + assert settings.run_event_stream_max_length == DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH == 5000 + assert settings.stream_text_delta_coalescing_enabled is True + assert settings.stream_text_delta_flush_interval_ms == 100 + assert settings.stream_text_delta_max_chars == 4096 + + +def test_server_settings_reads_run_event_bounds_from_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("DIFY_AGENT_RUN_EVENT_STREAM_MAX_LENGTH", "1234") + monkeypatch.setenv("DIFY_AGENT_STREAM_TEXT_DELTA_COALESCING_ENABLED", "false") + monkeypatch.setenv("DIFY_AGENT_STREAM_TEXT_DELTA_FLUSH_INTERVAL_MS", "250") + monkeypatch.setenv("DIFY_AGENT_STREAM_TEXT_DELTA_MAX_CHARS", "2048") + + settings = ServerSettings() + + assert settings.run_event_stream_max_length == 1234 + assert settings.stream_text_delta_coalescing_enabled is False + assert settings.stream_text_delta_flush_interval_ms == 250 + assert settings.stream_text_delta_max_chars == 2048 + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("run_event_stream_max_length", 0), + ("stream_text_delta_flush_interval_ms", 0), + ("stream_text_delta_max_chars", 0), + ], +) +def test_server_settings_rejects_invalid_run_event_bounds(field: str, value: int) -> None: + with pytest.raises(ValidationError): + _ = ServerSettings(**{field: value}) # pyright: ignore[reportArgumentType] + + def test_server_settings_reads_binding_file_download_command_timeout_from_env( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/dify-agent/tests/local/dify_agent/storage/test_redis_run_store.py b/dify-agent/tests/local/dify_agent/storage/test_redis_run_store.py index a39b960c7cc..68641aa7a10 100644 --- a/dify-agent/tests/local/dify_agent/storage/test_redis_run_store.py +++ b/dify-agent/tests/local/dify_agent/storage/test_redis_run_store.py @@ -24,7 +24,12 @@ from dify_agent.protocol.schemas import ( ) from dify_agent.runtime.cancellation import RunCancellationIntent from dify_agent.runtime.event_sink import RunFinalizationResult -from dify_agent.storage.redis_run_store import DEFAULT_RUN_RETENTION_SECONDS, RedisRunStore, RunNotFoundError +from dify_agent.storage.redis_run_store import ( + DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH, + DEFAULT_RUN_RETENTION_SECONDS, + RedisRunStore, + RunNotFoundError, +) class FakeRedis: @@ -48,18 +53,34 @@ class FakeRedis: self.commands.append(("get", key)) return self.values.get(key) - async def xadd(self, key: str, fields: Mapping[str, object]) -> str: - self.commands.append(("xadd", key, dict(fields))) - return self._append_stream_entry(key, fields) + async def xadd( + self, + key: str, + fields: Mapping[str, object], + *, + maxlen: int | None = None, + approximate: bool = True, + ) -> str: + self.commands.append(("xadd", key, dict(fields), maxlen, approximate)) + return self._append_stream_entry(key, fields, maxlen=maxlen) def pipeline(self, transaction: bool = True, shard_hint: str | None = None) -> "FakeRedisPipeline": self.commands.append(("pipeline", transaction, shard_hint)) return FakeRedisPipeline(self) - def _append_stream_entry(self, key: str, fields: Mapping[str, object]) -> str: + def _append_stream_entry( + self, + key: str, + fields: Mapping[str, object], + *, + maxlen: int | None = None, + ) -> str: entries = self.streams.setdefault(key, []) - event_id = f"{len(entries) + 1}-0" + next_sequence = self._stream_id_value(entries[-1][0])[0] + 1 if entries else 1 + event_id = f"{next_sequence}-0" entries.append((event_id, dict(fields))) + if maxlen is not None and len(entries) > maxlen: + del entries[: len(entries) - maxlen] self.stream_changed.set() return event_id @@ -137,9 +158,16 @@ class FakeRedisPipeline: async def __aexit__(self, exc_type: object, exc: object, traceback: object) -> None: del exc_type, exc, traceback - def xadd(self, key: str, fields: Mapping[str, object]) -> "FakeRedisPipeline": - self.redis.commands.append(("xadd", key, dict(fields))) - self.results.append(self.redis._append_stream_entry(key, fields)) + def xadd( + self, + key: str, + fields: Mapping[str, object], + *, + maxlen: int | None = None, + approximate: bool = True, + ) -> "FakeRedisPipeline": + self.redis.commands.append(("xadd", key, dict(fields), maxlen, approximate)) + self.results.append(self.redis._append_stream_entry(key, fields, maxlen=maxlen)) return self def expire(self, key: str, seconds: int) -> "FakeRedisPipeline": @@ -184,6 +212,15 @@ def test_create_run_writes_running_record_without_job_queue_and_with_retention() assert "request" not in str(redis.commands[0][2]) +@pytest.mark.parametrize( + "kwargs", + [{"run_retention_seconds": 0}, {"run_event_stream_max_length": 0}], +) +def test_rejects_non_positive_run_storage_bounds(kwargs: dict[str, int]) -> None: + with pytest.raises(ValueError, match="must be positive"): + _ = RedisRunStore(FakeRedis(), **kwargs) # pyright: ignore[reportArgumentType] + + def test_get_run_accepts_legacy_record_without_error_type() -> None: redis = FakeRedis() store = RedisRunStore(redis, prefix="test") # pyright: ignore[reportArgumentType] @@ -223,6 +260,7 @@ def test_request_cancellation_maps_eval_result_and_arguments() -> None: assert intent_payload["reason"] == "workflow_aborted" assert intent_payload["message"] == "workflow stopped" assert eval_command[7] == "60" + assert '"MAXLEN", "1"' in cast(str, eval_command[1]) def test_finalize_cancellation_maps_eval_result_and_arguments() -> None: @@ -262,6 +300,8 @@ def test_finalize_cancellation_maps_eval_result_and_arguments() -> None: assert payload["data"]["usage"]["completion_tokens"] == 8 assert payload["data"]["usage"]["total_tokens"] == 21 assert eval_command[10] == "60" + assert eval_command[11] == str(DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH) + assert '"MAXLEN", "~", ARGV[6]' in cast(str, eval_command[1]) def test_finalize_failed_run_maps_eval_result_and_arguments() -> None: @@ -290,6 +330,9 @@ def test_finalize_failed_run_maps_eval_result_and_arguments() -> None: assert eval_command[8:12] == ("1", "model failed", "1", "agent_run_limit_exceeded") payload = json.loads(cast(str, eval_command[12])) assert payload["data"]["error_type"] == "agent_run_limit_exceeded" + assert eval_command[13] == str(DEFAULT_RUN_RETENTION_SECONDS) + assert eval_command[14] == str(DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH) + assert '"MAXLEN", "~", ARGV[9]' in cast(str, eval_command[1]) def test_request_cancellation_raises_when_record_is_missing() -> None: @@ -368,6 +411,7 @@ def test_append_event_serializes_typed_event_without_id_and_expires_run_keys() - assert isinstance(fields, dict) assert '"id"' not in str(fields["payload"]) assert '"type":"run_started"' in str(fields["payload"]) + assert xadd_commands[0][3:] == (DEFAULT_RUN_EVENT_STREAM_MAX_LENGTH, True) expire_commands = {command for command in redis.commands if command[0] == "expire"} assert expire_commands == { ("expire", "test:runs:run-1:events", 60), @@ -376,6 +420,26 @@ def test_append_event_serializes_typed_event_without_id_and_expires_run_keys() - assert ("execute",) in redis.commands +def test_append_event_keeps_only_the_configured_number_of_entries() -> None: + redis = FakeRedis() + store = RedisRunStore( + redis, # pyright: ignore[reportArgumentType] + prefix="test", + run_retention_seconds=60, + run_event_stream_max_length=2, + ) + + async def scenario() -> None: + for _ in range(3): + _ = await store.append_event(RunStartedEvent(run_id="run-1")) + + asyncio.run(scenario()) + + assert len(redis.streams["test:runs:run-1:events"]) == 2 + xadd_commands = [command for command in redis.commands if command[0] == "xadd"] + assert all(command[3:] == (2, True) for command in xadd_commands) + + def test_get_events_round_trips_run_succeeded_output_and_session_snapshot() -> None: redis = FakeRedis() store = RedisRunStore(redis, prefix="test", run_retention_seconds=60) # pyright: ignore[reportArgumentType] diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index 9fa3bc98f46..0f32cb0a6bf 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -127,6 +127,30 @@ services: networks: - ssrf_proxy_network + # Local sandbox for Dify Agent shell workspaces (shellctl data plane). + # Exposes port 5004 on the host so a locally-run agent backend can reach it + # at http://localhost:5004 (DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT). + local_sandbox: + image: langgenius/dify-agent-local-sandbox:1.17.0 + restart: always + env_file: + - ./middleware.env + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + SHELLCTL_AUTH_TOKEN: ${DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN:-${DIFY_AGENT_SHELLCTL_AUTH_TOKEN:-}} + ports: + - "${EXPOSE_LOCAL_SANDBOX_PORT:-5004}:5004" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5004/healthz"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + volumes: + - dify_agent_local_sandbox_home:/home/dify + - dify_agent_local_sandbox_workspace:/workspace + # plugin daemon plugin_daemon: image: langgenius/dify-plugin-daemon:0.6.10-local @@ -259,3 +283,7 @@ networks: ssrf_proxy_network: driver: bridge internal: true + +volumes: + dify_agent_local_sandbox_home: + dify_agent_local_sandbox_workspace: diff --git a/docker/envs/core-services/dify-agent.env.example b/docker/envs/core-services/dify-agent.env.example index e4ab19342a3..c7ac2458c4e 100644 --- a/docker/envs/core-services/dify-agent.env.example +++ b/docker/envs/core-services/dify-agent.env.example @@ -8,7 +8,11 @@ AGENT_BACKEND_BASE_URL=http://agent_backend:5050 DIFY_AGENT_REDIS_URL= DIFY_AGENT_REDIS_PREFIX=dify-agent DIFY_AGENT_SHUTDOWN_GRACE_SECONDS=30 -DIFY_AGENT_RUN_RETENTION_SECONDS=259200 +DIFY_AGENT_RUN_RETENTION_SECONDS=7200 +DIFY_AGENT_RUN_EVENT_STREAM_MAX_LENGTH=5000 +DIFY_AGENT_STREAM_TEXT_DELTA_COALESCING_ENABLED=true +DIFY_AGENT_STREAM_TEXT_DELTA_FLUSH_INTERVAL_MS=100 +DIFY_AGENT_STREAM_TEXT_DELTA_MAX_CHARS=4096 # Internal deadline for the Agent Backend model/tool loop. The effective run # limit is whichever expires first: this value or the relevant API/Workflow # outer execution limit. To allow every path to run for one hour, also set diff --git a/docker/envs/middleware.env.example b/docker/envs/middleware.env.example index 3ff8139ad16..faf8307ff43 100644 --- a/docker/envs/middleware.env.example +++ b/docker/envs/middleware.env.example @@ -106,6 +106,12 @@ SANDBOX_HTTP_PROXY=http://ssrf_proxy:3128 SANDBOX_HTTPS_PROXY=http://ssrf_proxy:3128 SANDBOX_PORT=8194 +# ------------------------------ +# Environment Variables for local_sandbox Service (Dify Agent shell workspaces) +# ------------------------------ +# Leave empty to disable shellctl auth (local development default). +DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN= + # ------------------------------ # Environment Variables for ssrf_proxy Service # ------------------------------ @@ -145,6 +151,7 @@ EXPOSE_POSTGRES_PORT=5432 EXPOSE_MYSQL_PORT=3306 EXPOSE_REDIS_PORT=6379 EXPOSE_SANDBOX_PORT=8194 +EXPOSE_LOCAL_SANDBOX_PORT=5004 EXPOSE_SSRF_PROXY_PORT=3128 EXPOSE_WEAVIATE_PORT=8080 diff --git a/lint.config.ts b/lint.config.ts index b7689553430..2528c3e7c38 100644 --- a/lint.config.ts +++ b/lint.config.ts @@ -1117,6 +1117,19 @@ export const lintConfig = { 'eslint-react/use-memo': 'error', }, }, + { + files: ['web/**/*.{jsx,tsx}'], + excludeFiles: [ + 'web/**/__tests__/**', + 'web/**/*.spec.{jsx,tsx}', + 'web/**/*.test.{jsx,tsx}', + 'web/**/*.stories.{jsx,tsx}', + 'web/**/*.story.{jsx,tsx}', + ], + rules: { + 'dify/require-title-for-truncated-text': 'warn', + }, + }, { files: [ 'web/**/__tests__/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}', diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index e3acfb2612a..d7e91e0a61e 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -2626,9 +2626,6 @@ }, "jsx-a11y/no-static-element-interactions": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "web/app/components/plugins/plugin-auth/hooks/use-get-api.ts": { @@ -3853,11 +3850,6 @@ "count": 4 } }, - "web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/human-input/components/delivery-method/method-selector.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 2 @@ -4349,11 +4341,6 @@ "count": 4 } }, - "web/app/components/workflow/nodes/trigger-schedule/panel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx": { "no-restricted-imports": { "count": 1 diff --git a/package.json b/package.json index 88fed453dd4..68ec273fbba 100644 --- a/package.json +++ b/package.json @@ -56,12 +56,12 @@ "devEngines": { "runtime": { "name": "node", - "version": "^22.22.1", + "version": "24.20.0", "onFail": "download" } }, "engines": { - "node": "^22.22.1" + "node": "^24.20.0" }, - "packageManager": "pnpm@11.23.0" + "packageManager": "pnpm@11.25.0" } diff --git a/packages/contracts/generated/api/openapi/types.gen.ts b/packages/contracts/generated/api/openapi/types.gen.ts index d0a00673acf..ac9af4af710 100644 --- a/packages/contracts/generated/api/openapi/types.gen.ts +++ b/packages/contracts/generated/api/openapi/types.gen.ts @@ -355,6 +355,7 @@ export type OpenApiErrorCode = | 'request_entity_too_large' | 'too_many_files' | 'too_many_requests' + | 'trigger_workflow_service_mode_unavailable' | 'unauthorized' | 'unknown' | 'unsupported_file_type' diff --git a/packages/contracts/generated/api/openapi/zod.gen.ts b/packages/contracts/generated/api/openapi/zod.gen.ts index 47ae8d31995..55ddcc32f6e 100644 --- a/packages/contracts/generated/api/openapi/zod.gen.ts +++ b/packages/contracts/generated/api/openapi/zod.gen.ts @@ -446,6 +446,7 @@ export const zOpenApiErrorCode = z.enum([ 'request_entity_too_large', 'too_many_files', 'too_many_requests', + 'trigger_workflow_service_mode_unavailable', 'unauthorized', 'unknown', 'unsupported_file_type', diff --git a/packages/dev-proxy/package.json b/packages/dev-proxy/package.json index d249d0cf50f..ebdec46de78 100644 --- a/packages/dev-proxy/package.json +++ b/packages/dev-proxy/package.json @@ -39,6 +39,6 @@ "vitest": "catalog:" }, "engines": { - "node": "^22.22.1" + "node": "^24.20.0" } } diff --git a/packages/dev-proxy/vite.config.ts b/packages/dev-proxy/vite.config.ts index 2208c5108a6..d459331d695 100644 --- a/packages/dev-proxy/vite.config.ts +++ b/packages/dev-proxy/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ outDir: 'dist', platform: 'node', sourcemap: true, - target: 'node22', + target: 'node24', treeshake: true, }, test: { diff --git a/packages/dify-ui/AGENTS.md b/packages/dify-ui/AGENTS.md index 5d02acdc1e0..2cd05f418f9 100644 --- a/packages/dify-ui/AGENTS.md +++ b/packages/dify-ui/AGENTS.md @@ -20,9 +20,9 @@ then read only the guide for the contract being changed. - Imports, exports, naming, public types, generics, and anatomy: [Public API authoring] - Button and icon-only action behavior: [Button contract] and [Icon Button contract] - Compound input behavior: [Input Group contract] -- Form structure and labels: [Forms] +- Form structure, labels, and value ownership: [Forms] - Picker choice and typed values: [Selection] -- Portals, layering, and floating-surface semantics: [Overlays] +- Portals, presence, layering, and floating-surface semantics: [Overlays] - Tailwind integration and radius mapping: [Styling] - Package test ownership and setup: [Testing and development] diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md index 5825f310a82..622b4f556b4 100644 --- a/packages/dify-ui/README.md +++ b/packages/dify-ui/README.md @@ -70,9 +70,9 @@ Upstream behavior remains owned by the [Base UI documentation]. | Guide | Scope | | ------------------------- | ------------------------------------------------------------------------------ | -| [Forms] | Native submit boundaries, fields, labels, grouped controls, and errors. | +| [Forms] | Native submit boundaries, value ownership, fields, labels, and errors. | | [Selection] | Typed values and choosing among segmented controls, pickers, and radio groups. | -| [Overlays] | Portals, root isolation, layering, trigger composition, and semantics. | +| [Overlays] | Portals, presence lifecycles, layering, trigger composition, and semantics. | | [Styling] | Tailwind CSS integration and the Figma radius mapping. | | [Public API authoring] | Subpath exports, naming, public types, generics, and private helpers. | | [Testing and development] | Package commands, test ownership, accessibility, and animation setup. | diff --git a/packages/dify-ui/docs/forms.md b/packages/dify-ui/docs/forms.md index f3d3f21c1fe..862b207ed74 100644 --- a/packages/dify-ui/docs/forms.md +++ b/packages/dify-ui/docs/forms.md @@ -16,6 +16,22 @@ remains correct when another form library owns submission and validation; do not Set [`Button`] submit buttons to `type="submit"` explicitly. Keep every other button inside a form at `type="button"`. +## Value and state ownership + +`Form` owns the submission and validation boundary described above, not each field's draft. + +Choose controlledness from source-of-truth needs, independently from where a draft is stored. +Prefer `defaultValue` when application React code does not need to own the current value. Use +`value` and change handlers when application rendering or coordination must own it while editing. +Listening to change events, tracking dirty state, and native or primitive validation do not by +themselves require controlled state. + +Application code owns the draft in the narrowest component whose lifetime matches it. A value can +be controlled locally without being lifted. An uncontrolled field can participate in a persisted +workflow when that workflow captures its value at an explicit persistence boundary. The +surrounding surface defines its mount lifecycle; owner placement determines whether draft state +lives inside or outside that lifecycle. + ## Fields and labels Use `Field` when a control needs a shared name, label, validation, description, or error state. A @@ -61,9 +77,9 @@ option with `FieldItem` and give it its own label: Every radio belongs to a `RadioGroup`. Use `FieldsetLegend` to name the group and `FieldLabel` to name each option; do not render a standalone `Radio`. -Keep form state, schemas, server validation, and reset behavior outside these primitives. Pass -their observable state through the public field and control props instead of replacing the -semantic structure. +Keep form state, schemas, server validation, and reset behavior outside the primitive internals, +in the nearest application owner with the required lifetime. Pass observable state through the +public field and control props instead of replacing the semantic structure. [Base UI Slider anatomy]: https://base-ui.com/react/components/slider#anatomy [Base UI forms handbook]: https://base-ui.com/react/handbook/forms diff --git a/packages/dify-ui/docs/overlays.md b/packages/dify-ui/docs/overlays.md index 422ce4621f5..1274ce1b274 100644 --- a/packages/dify-ui/docs/overlays.md +++ b/packages/dify-ui/docs/overlays.md @@ -10,6 +10,25 @@ Floating surfaces render through [Base UI Portal] into `document.body`. Convenie such as `DialogContent`, `PopoverContent`, and `SelectContent` own their portals internally; primitives with explicit anatomy expose the constituent portal and content parts. +## Mounting and state lifetime + +An overlay root's React lifetime, its open state, and its portal subtree's presence are separate. +Presence is part of each primitive's contract. Convenience content follows its primitive's +default mount behavior; for example, `DialogContent` owns a [`Dialog.Portal`] whose subtree mounts +when the dialog opens and unmounts after any close transition completes. The `Dialog` root may +remain mounted and controlled independently of that content lifetime. Removing the controlled +root with the same condition that closes it bypasses the primitive's closing lifecycle. + +Application code can use an unmounting content subtree as the owner of state scoped to one mounted +content session. State that must survive the subtree's unmount belongs to an explicit longer-lived +feature owner. +Unmounting resets only DOM and component state owned inside that subtree; state declared by an +ancestor or external store survives. Portal placement alone is not a reset boundary. +Consumers using explicit anatomy may opt into `keepMounted` where that portal supports it; they +must then define which state persists and which state resets instead of relying on a remount. +Check the selected primitive's API rather than assuming every overlay portal has the same presence +options. + The host must establish an isolated stacking context at its application root: ```tsx @@ -58,6 +77,7 @@ spacing unless its API documents a measured exception. [Base UI Portal]: https://base-ui.com/react/overview/quick-start#portals [MDN `isolation`]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/isolation +[`Dialog.Portal`]: https://base-ui.com/react/components/dialog#portal [`Popover`]: https://base-ui.com/react/components/popover [`PreviewCard`]: https://base-ui.com/react/components/preview-card [`Tooltip`]: https://base-ui.com/react/components/tooltip diff --git a/packages/migrate-no-unchecked-indexed-access/vite.config.ts b/packages/migrate-no-unchecked-indexed-access/vite.config.ts index ac4aed1a064..2e167da96a0 100644 --- a/packages/migrate-no-unchecked-indexed-access/vite.config.ts +++ b/packages/migrate-no-unchecked-indexed-access/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ outDir: 'dist', platform: 'node', sourcemap: true, - target: 'node22', + target: 'node24', treeshake: true, }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 114f415e7a1..429790217f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,11 +7,11 @@ settings: catalogs: default: '@amplitude/analytics-browser': - specifier: 2.45.6 - version: 2.45.6 + specifier: 2.45.8 + version: 2.45.8 '@amplitude/plugin-session-replay-browser': - specifier: 1.33.8 - version: 1.33.8 + specifier: 1.35.0 + version: 1.35.0 '@axe-core/playwright': specifier: 4.13.0 version: 4.13.0 @@ -79,8 +79,8 @@ catalogs: specifier: 0.47.0 version: 0.47.0 '@mediabunny/mp3-encoder': - specifier: 1.55.2 - version: 1.55.2 + specifier: 1.55.5 + version: 1.55.5 '@monaco-editor/react': specifier: 4.7.0 version: 4.7.0 @@ -109,8 +109,8 @@ catalogs: specifier: 4.2.3 version: 4.2.3 '@sentry/react': - specifier: 10.70.0 - version: 10.70.0 + specifier: 10.73.0 + version: 10.73.0 '@storybook/addon-a11y': specifier: 10.5.10 version: 10.5.10 @@ -157,14 +157,14 @@ catalogs: specifier: 4.3.3 version: 4.3.3 '@tanstack/eslint-plugin-query': - specifier: 5.102.2 - version: 5.102.2 + specifier: 5.102.8 + version: 5.102.8 '@tanstack/form-core': specifier: 1.33.5 version: 1.33.5 '@tanstack/query-core': - specifier: 5.102.2 - version: 5.102.2 + specifier: 5.102.8 + version: 5.102.8 '@tanstack/react-form': specifier: 1.33.5 version: 1.33.5 @@ -172,8 +172,8 @@ catalogs: specifier: 0.10.0 version: 0.10.0 '@tanstack/react-query': - specifier: 5.102.2 - version: 5.102.2 + specifier: 5.102.8 + version: 5.102.8 '@tanstack/react-virtual': specifier: 3.14.10 version: 3.14.10 @@ -184,8 +184,8 @@ catalogs: specifier: 6.9.1 version: 6.9.1 '@testing-library/react': - specifier: 16.3.2 - version: 16.3.2 + specifier: 16.3.3 + version: 16.3.3 '@testing-library/user-event': specifier: 14.6.6 version: 14.6.6 @@ -223,14 +223,14 @@ catalogs: specifier: 1.15.9 version: 1.15.9 '@typescript-eslint/parser': - specifier: 8.67.0 - version: 8.67.0 + specifier: 8.69.0 + version: 8.69.0 '@typescript/native': specifier: npm:typescript@7.0.2 version: 7.0.2 '@vitejs/plugin-react': - specifier: 6.1.0 - version: 6.1.0 + specifier: 6.1.1 + version: 6.1.1 '@vitejs/plugin-rsc': specifier: 0.5.34 version: 0.5.34 @@ -307,11 +307,11 @@ catalogs: specifier: 5.6.0 version: 5.6.0 es-toolkit: - specifier: 1.51.0 - version: 1.51.0 + specifier: 1.52.0 + version: 1.52.0 eslint: - specifier: 10.9.0 - version: 10.9.0 + specifier: 10.9.1 + version: 10.9.1 eslint-markdown: specifier: 0.12.1 version: 0.12.1 @@ -343,8 +343,8 @@ catalogs: specifier: 1.3.1 version: 1.3.1 eslint-plugin-perfectionist: - specifier: 5.10.1 - version: 5.10.1 + specifier: 5.11.0 + version: 5.11.0 eslint-plugin-pnpm: specifier: 1.8.0 version: 1.8.0 @@ -370,20 +370,20 @@ catalogs: specifier: 3.1.3 version: 3.1.3 foxact: - specifier: 0.3.9 - version: 0.3.9 + specifier: 0.3.10 + version: 0.3.10 fuse.js: specifier: 7.5.0 version: 7.5.0 happy-dom: - specifier: 20.11.6 - version: 20.11.6 + specifier: 20.12.0 + version: 20.12.0 hast-util-to-jsx-runtime: specifier: 2.3.6 version: 2.3.6 hono: - specifier: 4.13.3 - version: 4.13.3 + specifier: 4.13.5 + version: 4.13.5 html-entities: specifier: 2.6.0 version: 2.6.0 @@ -403,8 +403,8 @@ catalogs: specifier: 11.1.18 version: 11.1.18 jotai: - specifier: 2.20.2 - version: 2.20.2 + specifier: 2.20.3 + version: 2.20.3 jotai-scope: specifier: 0.11.0 version: 0.11.0 @@ -415,8 +415,8 @@ catalogs: specifier: 3.0.8 version: 3.0.8 js-yaml: - specifier: 5.3.0 - version: 5.3.0 + specifier: 5.4.1 + version: 5.4.1 jsonschema: specifier: 1.5.0 version: 1.5.0 @@ -424,11 +424,11 @@ catalogs: specifier: 0.17.0 version: 0.17.0 knip: - specifier: 6.32.2 - version: 6.32.2 + specifier: 6.34.0 + version: 6.34.0 ky: - specifier: 2.0.2 - version: 2.0.2 + specifier: 2.1.0 + version: 2.1.0 lexical: specifier: 0.47.0 version: 0.47.0 @@ -436,14 +436,14 @@ catalogs: specifier: 1.0.4 version: 1.0.4 loro-crdt: - specifier: 1.14.1 - version: 1.14.1 + specifier: 1.15.1 + version: 1.15.1 mediabunny: - specifier: 1.55.2 - version: 1.55.2 + specifier: 1.55.5 + version: 1.55.5 mermaid: - specifier: 11.17.0 - version: 11.17.0 + specifier: 11.17.2 + version: 11.17.2 mime: specifier: 4.1.0 version: 4.1.0 @@ -457,17 +457,17 @@ catalogs: specifier: 1.1.0 version: 1.1.0 next: - specifier: 16.3.3 - version: 16.3.3 + specifier: 16.3.4 + version: 16.3.4 next-themes: specifier: 0.4.6 version: 0.4.6 nuqs: - specifier: 2.10.0 - version: 2.10.0 + specifier: 2.10.1 + version: 2.10.1 open: - specifier: 11.0.1 - version: 11.0.1 + specifier: 11.0.2 + version: 11.0.2 ora: specifier: 9.4.1 version: 9.4.1 @@ -487,8 +487,8 @@ catalogs: specifier: 4.2.0 version: 4.2.0 qs: - specifier: 6.15.3 - version: 6.15.3 + specifier: 6.16.0 + version: 6.16.0 react: specifier: 19.2.8 version: 19.2.8 @@ -532,8 +532,8 @@ catalogs: specifier: 0.0.1 version: 0.0.1 shiki: - specifier: 4.4.2 - version: 4.4.2 + specifier: 4.4.3 + version: 4.4.3 socket.io-client: specifier: 4.8.3 version: 4.8.3 @@ -547,8 +547,8 @@ catalogs: specifier: 10.5.10 version: 10.5.10 streamdown: - specifier: 2.5.0 - version: 2.5.0 + specifier: 2.6.0 + version: 2.6.0 string-ts: specifier: 2.3.1 version: 2.3.1 @@ -559,11 +559,11 @@ catalogs: specifier: 4.3.3 version: 4.3.3 tldts: - specifier: 7.4.10 - version: 7.4.10 + specifier: 7.4.11 + version: 7.4.11 tsx: - specifier: 4.23.12 - version: 4.23.12 + specifier: 4.23.13 + version: 4.23.13 typescript: specifier: npm:@typescript/typescript6@6.0.2 version: 6.0.2 @@ -604,8 +604,8 @@ catalogs: specifier: 1.1.5 version: 1.1.5 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.5.4 + version: 4.5.4 zundo: specifier: 2.3.0 version: 2.3.0 @@ -622,11 +622,11 @@ overrides: esbuild@>=0.27.3 <0.28.1: ^0.28.2 is-core-module: npm:@nolyfill/is-core-module@^1.0.39 js-yaml@<=4.1.1: ^4.1.2 - picomatch@>=4.0.0 <4.0.4: 4.0.5 + picomatch@>=4.0.0 <4.0.4: 4.0.7 postcss-selector-parser@>=6.0.0 <6.1.3: 6.1.4 postcss-selector-parser@>=7.0.0 <7.1.3: 7.1.5 postcss@<8.5.10: ^8.5.10 - rollup@>=4.0.0 <4.59.0: 4.62.5 + rollup@>=4.0.0 <4.59.0: 4.63.1 safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 side-channel: npm:@nolyfill/side-channel@^1.0.44 solid-js: 1.9.15 @@ -644,10 +644,10 @@ importers: devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 'catalog:' - version: 4.7.2(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 4.7.2(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) '@eslint-react/eslint-plugin': specifier: 'catalog:' - version: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) '@eslint/markdown': specifier: 'catalog:' version: 8.0.3(supports-color@11.0.0) @@ -659,13 +659,13 @@ importers: version: 1.2.10 '@tanstack/eslint-plugin-query': specifier: 'catalog:' - version: 5.102.2(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 5.102.8(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) '@types/node': specifier: 'catalog:' version: 25.9.5 '@typescript-eslint/parser': specifier: 'catalog:' - version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) '@typescript/native': specifier: 'catalog:' version: typescript@7.0.2 @@ -674,73 +674,73 @@ importers: version: 10.0.5 eslint: specifier: 'catalog:' - version: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + version: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) eslint-markdown: specifier: 'catalog:' - version: 0.12.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 0.12.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-antfu: specifier: 'catalog:' - version: 3.2.3(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 3.2.3(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-better-tailwindcss: specifier: 'catalog:' - version: 4.7.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(tailwindcss@4.3.3) + version: 4.7.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(tailwindcss@4.3.3) eslint-plugin-command: specifier: 'catalog:' - version: 3.5.3(@typescript-eslint/typescript-estree@8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0))(@typescript-eslint/utils@8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 3.5.3(@typescript-eslint/typescript-estree@8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0))(@typescript-eslint/utils@8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-erasable-syntax-only: specifier: 'catalog:' - version: 0.4.2(@typescript-eslint/parser@8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 0.4.2(@typescript-eslint/parser@8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-jsdoc: specifier: 'catalog:' - version: 63.3.3(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 63.3.3(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-jsonc: specifier: 'catalog:' - version: 3.4.2(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 3.4.2(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-markdown-preferences: specifier: 'catalog:' - version: 0.41.1(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 0.41.1(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-n: specifier: 'catalog:' - version: 18.3.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 18.3.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-no-barrel-files: specifier: 'catalog:' - version: 1.3.1(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 1.3.1(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-perfectionist: specifier: 'catalog:' - version: 5.10.1(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 5.11.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-pnpm: specifier: 'catalog:' - version: 1.8.0(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 1.8.0(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-regexp: specifier: 'catalog:' - version: 3.1.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 3.1.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-storybook: specifier: 'catalog:' - version: 10.5.10(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 10.5.10(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-toml: specifier: 'catalog:' - version: 1.5.0(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + version: 1.5.0(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-unicorn: specifier: 'catalog:' - version: 71.1.0(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 71.1.0(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) eslint-plugin-yml: specifier: 'catalog:' - version: 3.8.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + version: 3.8.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) knip: specifier: 'catalog:' - version: 6.32.2 + version: 6.34.0 node: - specifier: runtime:^22.22.1 - version: runtime:22.23.2 + specifier: runtime:24.20.0 + version: runtime:24.20.0 typescript: specifier: 'catalog:' version: '@typescript/typescript6@6.0.2' vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) cli: dependencies: @@ -767,13 +767,13 @@ importers: version: 3.1.1 js-yaml: specifier: 'catalog:' - version: 5.3.0 + version: 5.4.1 lockfile: specifier: 'catalog:' version: 1.0.4 open: specifier: 'catalog:' - version: 11.0.1 + version: 11.0.2 ora: specifier: 'catalog:' version: 9.4.1 @@ -788,14 +788,14 @@ importers: version: 7.29.0 zod: specifier: 'catalog:' - version: 4.4.3 + version: 4.5.4 devDependencies: '@dify/tsconfig': specifier: workspace:* version: link:../packages/tsconfig '@hono/node-server': specifier: 'catalog:' - version: 2.1.1(hono@4.13.3) + version: 2.1.1(hono@4.13.5) '@types/lockfile': specifier: 'catalog:' version: 1.0.4 @@ -810,19 +810,19 @@ importers: version: 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11) hono: specifier: 'catalog:' - version: 4.13.3 + version: 4.13.5 typescript: specifier: 'catalog:' version: '@typescript/typescript6@6.0.2' vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) e2e: devDependencies: @@ -852,7 +852,7 @@ importers: version: 1.62.1 '@t3-oss/env-core': specifier: 'catalog:' - version: 0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.4.3) + version: 0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.5.4) '@types/node': specifier: 'catalog:' version: 25.9.5 @@ -864,22 +864,22 @@ importers: version: 3.1.1 tsx: specifier: 'catalog:' - version: 4.23.12 + version: 4.23.13 typescript: specifier: 'catalog:' version: '@typescript/typescript6@6.0.2' vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) zod: specifier: 'catalog:' - version: 4.4.3 + version: 4.5.4 packages/contracts: dependencies: @@ -888,7 +888,7 @@ importers: version: 1.15.0 zod: specifier: 'catalog:' - version: 4.4.3 + version: 4.5.4 devDependencies: '@dify/tsconfig': specifier: workspace:* @@ -904,25 +904,25 @@ importers: version: typescript@7.0.2 js-yaml: specifier: 'catalog:' - version: 5.3.0 + version: 5.4.1 typescript: specifier: 'catalog:' version: '@typescript/typescript6@6.0.2' vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) packages/dev-proxy: dependencies: '@hono/node-server': specifier: 'catalog:' - version: 2.1.1(hono@4.13.3) + version: 2.1.1(hono@4.13.5) c12: specifier: 'catalog:' version: 4.0.0-beta.5(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.0)(jiti@2.7.0)(magicast@0.5.3) @@ -931,7 +931,7 @@ importers: version: 5.0.0 hono: specifier: 'catalog:' - version: 4.13.3 + version: 4.13.5 devDependencies: '@dify/tsconfig': specifier: workspace:* @@ -944,13 +944,13 @@ importers: version: typescript@7.0.2 vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.11.6) + version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.12.0) packages/dify-ui: dependencies: @@ -966,7 +966,7 @@ importers: version: 1.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@chromatic-com/storybook': specifier: 'catalog:' - version: 5.3.0(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 5.3.0(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@dify/tsconfig': specifier: workspace:* version: link:../tsconfig @@ -978,25 +978,25 @@ importers: version: 1.2.10 '@storybook/addon-a11y': specifier: 'catalog:' - version: 10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@storybook/addon-links': specifier: 'catalog:' - version: 10.5.10(@types/react@19.2.18)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 10.5.10(@types/react@19.2.18)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@storybook/addon-themes': specifier: 'catalog:' - version: 10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.5.10(@vitest/browser-playwright@4.1.11)(@vitest/browser@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11))(@vitest/runner@4.1.11)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(vitest@4.1.11) + version: 10.5.10(@vitest/browser-playwright@4.1.11)(@vitest/browser@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11))(@vitest/runner@4.1.11)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(vitest@4.1.11) '@storybook/react-vite': specifier: 'catalog:' - version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0) + version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0) '@tailwindcss/vite': specifier: 'catalog:' - version: 4.3.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.3.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@tanstack/react-hotkeys': specifier: 'catalog:' version: 0.10.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1014,10 +1014,10 @@ importers: version: typescript@7.0.2 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.1.0(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 6.1.1(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/browser-playwright': specifier: 'catalog:' - version: 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) + version: 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11) @@ -1035,7 +1035,7 @@ importers: version: 19.2.8(react@19.2.8) storybook: specifier: 'catalog:' - version: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) tailwindcss: specifier: 'catalog:' version: 4.3.3 @@ -1044,13 +1044,13 @@ importers: version: '@typescript/typescript6@6.0.2' vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) vitest-browser-react: specifier: 'catalog:' version: 2.2.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11) @@ -1062,7 +1062,7 @@ importers: version: 0.2.0(supports-color@11.0.0) tsx: specifier: 'catalog:' - version: 4.23.12 + version: 4.23.13 packages/jotai-tanstack-form: devDependencies: @@ -1077,19 +1077,19 @@ importers: version: typescript@7.0.2 jotai: specifier: 'catalog:' - version: 2.20.2(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8) + version: 2.20.3(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8) typescript: specifier: 'catalog:' version: '@typescript/typescript6@6.0.2' vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) packages/migrate-no-unchecked-indexed-access: dependencies: @@ -1108,10 +1108,10 @@ importers: version: 25.9.5 vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) packages/tsconfig: {} @@ -1134,22 +1134,22 @@ importers: version: '@typescript/typescript6@6.0.2' vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) web: dependencies: '@amplitude/analytics-browser': specifier: 'catalog:' - version: 2.45.6 + version: 2.45.8 '@amplitude/plugin-session-replay-browser': specifier: 'catalog:' - version: 1.33.8(@amplitude/rrweb@2.1.1) + version: 1.35.0(@amplitude/rrweb@2.1.1) '@base-ui/react': specifier: 'catalog:' version: 1.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1188,7 +1188,7 @@ importers: version: 0.47.0(@typescript/typescript6@6.0.2) '@mediabunny/mp3-encoder': specifier: 'catalog:' - version: 1.55.2(mediabunny@1.55.2) + version: 1.55.5(mediabunny@1.55.5) '@monaco-editor/react': specifier: 'catalog:' version: 4.7.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1203,13 +1203,13 @@ importers: version: 1.15.0 '@orpc/tanstack-query': specifier: 'catalog:' - version: 1.15.0(@orpc/client@1.15.0)(@tanstack/query-core@5.102.2) + version: 1.15.0(@orpc/client@1.15.0)(@tanstack/query-core@5.102.8) '@remixicon/react': specifier: 'catalog:' version: 4.9.0(react@19.2.8) '@sentry/react': specifier: 'catalog:' - version: 10.70.0(react@19.2.8) + version: 10.73.0(react@19.2.8) '@streamdown/math': specifier: 'catalog:' version: 1.0.2(react@19.2.8)(supports-color@11.0.0) @@ -1218,10 +1218,10 @@ importers: version: 3.2.8 '@t3-oss/env-nextjs': specifier: 'catalog:' - version: 0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.4.3) + version: 0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.5.4) '@tanstack/query-core': specifier: 'catalog:' - version: 5.102.2 + version: 5.102.8 '@tanstack/react-form': specifier: 'catalog:' version: 1.33.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1230,7 +1230,7 @@ importers: version: 0.10.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tanstack/react-query': specifier: 'catalog:' - version: 5.102.2(react@19.2.8) + version: 5.102.8(react@19.2.8) '@tanstack/react-virtual': specifier: 'catalog:' version: 3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1281,13 +1281,13 @@ importers: version: 5.6.0 es-toolkit: specifier: 'catalog:' - version: 1.51.0 + version: 1.52.0 fast-deep-equal: specifier: 'catalog:' version: 3.1.3 foxact: specifier: 'catalog:' - version: 0.3.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 0.3.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8) fuse.js: specifier: 'catalog:' version: 7.5.0 @@ -1311,19 +1311,19 @@ importers: version: 11.1.18 jotai: specifier: 'catalog:' - version: 2.20.2(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8) + version: 2.20.3(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8) jotai-scope: specifier: 'catalog:' - version: 0.11.0(jotai@2.20.2(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) + version: 0.11.0(jotai@2.20.3(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) jotai-tanstack-query: specifier: 'catalog:' - version: 0.11.0(@tanstack/query-core@5.102.2)(@tanstack/react-query@5.102.2(react@19.2.8))(jotai@2.20.2(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) + version: 0.11.0(@tanstack/query-core@5.102.8)(@tanstack/react-query@5.102.8(react@19.2.8))(jotai@2.20.3(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) js-cookie: specifier: 'catalog:' version: 3.0.8 js-yaml: specifier: 'catalog:' - version: 5.3.0 + version: 5.4.1 jsonschema: specifier: 'catalog:' version: 1.5.0 @@ -1332,19 +1332,19 @@ importers: version: 0.17.0 ky: specifier: 'catalog:' - version: 2.0.2 + version: 2.1.0 lexical: specifier: 'catalog:' version: 0.47.0(@typescript/typescript6@6.0.2) loro-crdt: specifier: 'catalog:' - version: 1.14.1 + version: 1.15.1 mediabunny: specifier: 'catalog:' - version: 1.55.2 + version: 1.55.5 mermaid: specifier: 'catalog:' - version: 11.17.0 + version: 11.17.2 mime: specifier: 'catalog:' version: 4.1.0 @@ -1359,13 +1359,13 @@ importers: version: 1.1.0 next: specifier: 'catalog:' - version: 16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) nuqs: specifier: 'catalog:' - version: 2.10.0(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + version: 2.10.1(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) pinyin-pro: specifier: 'catalog:' version: 3.29.3 @@ -1374,7 +1374,7 @@ importers: version: 4.2.0(react@19.2.8) qs: specifier: 'catalog:' - version: 6.15.3 + version: 6.16.0 react: specifier: 'catalog:' version: 19.2.8 @@ -1416,7 +1416,7 @@ importers: version: 0.0.1 shiki: specifier: 'catalog:' - version: 4.4.2 + version: 4.4.3 socket.io-client: specifier: 'catalog:' version: 4.8.3(supports-color@11.0.0) @@ -1428,13 +1428,13 @@ importers: version: 1.0.8 streamdown: specifier: 'catalog:' - version: 2.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@11.0.0) + version: 2.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@11.0.0) string-ts: specifier: 'catalog:' version: 2.3.1 tldts: specifier: 'catalog:' - version: 7.4.10 + version: 7.4.11 unist-util-visit: specifier: 'catalog:' version: 5.1.0 @@ -1446,7 +1446,7 @@ importers: version: 14.0.2 zod: specifier: 'catalog:' - version: 4.4.3 + version: 4.5.4 zundo: specifier: 'catalog:' version: 2.3.0(zustand@5.0.15(@types/react@19.2.18)(immer@11.1.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))) @@ -1456,7 +1456,7 @@ importers: devDependencies: '@chromatic-com/storybook': specifier: 'catalog:' - version: 5.3.0(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 5.3.0(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@dify/contracts': specifier: workspace:* version: link:../packages/contracts @@ -1486,28 +1486,28 @@ importers: version: 4.2.3 '@storybook/addon-docs': specifier: 'catalog:' - version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@storybook/addon-links': specifier: 'catalog:' - version: 10.5.10(@types/react@19.2.18)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 10.5.10(@types/react@19.2.18)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@storybook/addon-onboarding': specifier: 'catalog:' - version: 10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@storybook/addon-themes': specifier: 'catalog:' - version: 10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + version: 10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@storybook/nextjs-vite': specifier: 'catalog:' - version: 10.5.10(@babel/core@7.29.7(supports-color@11.0.0))(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0) + version: 10.5.10(@babel/core@7.29.7(supports-color@11.0.0))(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0) '@storybook/react': specifier: 'catalog:' - version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0) + version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0) '@tailwindcss/postcss': specifier: 'catalog:' version: 4.3.3 '@tailwindcss/vite': specifier: 'catalog:' - version: 4.3.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.3.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -1516,7 +1516,7 @@ importers: version: 6.9.1 '@testing-library/react': specifier: 'catalog:' - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@testing-library/user-event': specifier: 'catalog:' version: 14.6.6(@testing-library/dom@10.4.1) @@ -1555,13 +1555,13 @@ importers: version: typescript@7.0.2 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.1.0(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 6.1.1(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + version: 0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) '@vitest/browser-playwright': specifier: 'catalog:' - version: 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) + version: 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11) @@ -1573,7 +1573,7 @@ importers: version: 1.6.6(supports-color@11.0.0) happy-dom: specifier: 'catalog:' - version: 20.11.6 + version: 20.12.0 playwright: specifier: 'catalog:' version: 1.62.1 @@ -1585,13 +1585,13 @@ importers: version: 19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8) storybook: specifier: 'catalog:' - version: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) tailwindcss: specifier: 'catalog:' version: 4.3.3 tsx: specifier: 'catalog:' - version: 4.23.12 + version: 4.23.13 typescript: specifier: 'catalog:' version: '@typescript/typescript6@6.0.2' @@ -1600,19 +1600,19 @@ importers: version: 3.19.3 vinext: specifier: 'catalog:' - version: 1.0.0-beta.8(@vitejs/plugin-react@6.1.0(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + version: 1.0.0-beta.8(@vitejs/plugin-react@6.1.1(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plugin-inspect: specifier: 'catalog:' - version: 12.0.2(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(srvx@0.12.5) + version: 12.0.2(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(srvx@0.12.5) vite-plus: specifier: 'catalog:' - version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + version: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + version: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) vitest-browser-react: specifier: 'catalog:' version: 2.2.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11) @@ -1629,50 +1629,47 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@amplitude/analytics-browser@2.45.6': - resolution: {integrity: sha512-sq03ROnttLP6i3I8LXGkAqtZAoi2Y7k5Mhx3PLYiB0mwTPCcbYaW4CyR0V52NI+7BFcpsD8DxF2ZeCoxBdWGqQ==} - - '@amplitude/analytics-client-common@2.4.60': - resolution: {integrity: sha512-YdvEsxkqPk4rkw3zdJGP92wE+CQwjqDU0hI3eX/GABqivZ8HZ8VBmot7WyZPOSzGSr+FxuwwC3ZpVTY5K8OEjQ==} + '@amplitude/analytics-browser@2.45.8': + resolution: {integrity: sha512-BdRLCcKGwQq1+1x2QOdHu5PJDMkIr+fvbbcQwO6lCsTCG44PzpL38n5jW9JNDhP88nHoz+lKyNkLDN3yd98pJA==} '@amplitude/analytics-connector@1.6.4': resolution: {integrity: sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==} - '@amplitude/analytics-core@2.54.2': - resolution: {integrity: sha512-kXFatnKxQMABvr9D43BV0C6TI4gAdksXS925e9dvAbu76QwLQhWYttJHCXMdC4jYn7+QdshhCNFp6677mQNSLA==} + '@amplitude/analytics-core@2.55.0': + resolution: {integrity: sha512-fzjXEWX2o5I58Tl5jYaw2G5Tp5ejfcDkayWMogEThCl/fd6AqRKkrl/dVVqGV3h/GS3HZws7J4baIWkaq92jkQ==} '@amplitude/analytics-types@2.11.1': resolution: {integrity: sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A==} - '@amplitude/element-selector@0.2.1': - resolution: {integrity: sha512-QZYvhO2cyaw6B7gFWu95QrE/l2a5oSCHgYR2S6DqZ6/NVDWIzbBtEjwLcxZFPXbO+DM1Rng6GUNljMNFtdlFDw==} + '@amplitude/element-selector@0.3.0': + resolution: {integrity: sha512-rfjXGjXqQiitxse3ZcoPLny2Uy8AXYnWAqB6oqZj39zYVuieoxhfWT50HZks3F4Ny4Zyf45tJqyLCnfvGGMs4A==} '@amplitude/experiment-core@0.7.2': resolution: {integrity: sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA==} - '@amplitude/plugin-autocapture-browser@1.28.11': - resolution: {integrity: sha512-hmdzNrWgFt0y35RHUxohJx2jgaN7m/bJUvSWv4hIz4LZ8yRA0cPfPnWhdF58HaTnpk7GjplwOvxDcjO1BBHCRQ==} + '@amplitude/plugin-autocapture-browser@1.29.0': + resolution: {integrity: sha512-S+9qjMV0jGu7KoYjPeLoeGavKjdjyNqx2gMLnJQQMTi1HaaOZZFT0Y36TyRfbYuqVL3osSe3aUnoejmuDTx14w==} - '@amplitude/plugin-custom-enrichment-browser@0.1.21': - resolution: {integrity: sha512-B5JxPtA8/Bt6Ypdx9p44Y/yyDQOINyQk0MbEeiN/OGU4kkbgiPgSMNe7X4QxDlXWtViYOwGn25RrIQyXnajjwA==} + '@amplitude/plugin-custom-enrichment-browser@0.1.22': + resolution: {integrity: sha512-KDTVCOmivrLzp1Q9EJuU61Yzw0Mip9DU/VnMnfROD37rCv0QX78O7RuYVzN+R6sprgUw+tkmS512PmkbVLOFsA==} - '@amplitude/plugin-event-property-attribution-browser@0.2.13': - resolution: {integrity: sha512-7T8Ci3p8o/9UVhyeHXhWQossPVTgou8KwFAD8ntVKUkN9PvZD6I2YJgFWqtmygJfGUGpgv34OcGyAgbofsFiAQ==} + '@amplitude/plugin-event-property-attribution-browser@0.2.14': + resolution: {integrity: sha512-uFuKp4sADFMLgh6vM/+c8RC6AXpMwcYVD9ZKkTjQn6tTz20cct1AFEjnnFc7/n9oT0D8EKvUUE7SsLg+V55QvQ==} - '@amplitude/plugin-network-capture-browser@1.10.13': - resolution: {integrity: sha512-X6KynnE6wxL3dT1A23T5FQzHpKAbR0vRx69yGi3CHFSL1IVZNbQR4MMh7dVg6Ev+JGnJs7tCn+EZ8a5mYD4lIg==} + '@amplitude/plugin-network-capture-browser@1.10.14': + resolution: {integrity: sha512-MGRgT89KaPDvrZXMp+5rWgF5GTC7YkCj/gIgHZ22FgH1CrejwHhA5FWTurW28f48rOgZ+V1Ruqab70/sst4KJQ==} - '@amplitude/plugin-page-url-enrichment-browser@0.7.23': - resolution: {integrity: sha512-c3ISLxTeYlghnyH4axNZh4Jex30QwetYEL5J7kubs3S9MYqwVCFF5RoHTFwhm3WrTjOYJk2W6qWindjXbrS85A==} + '@amplitude/plugin-page-url-enrichment-browser@0.7.24': + resolution: {integrity: sha512-kYwEBh+ssdbdqslGHVV3/m0ao139HWjd4SOwBZ47EQjeYXRDzNAw752iQIr1vx/+zRRdEOLQHO3GS1JGdWSIKw==} - '@amplitude/plugin-page-view-tracking-browser@2.11.13': - resolution: {integrity: sha512-cv+crjWw51mjX0DJ4QRNGZruZC91Lhn5MSdc9XaO6h8YqDokWMbg3wpLzcU2ds0d5l3iep9qfjsx6vzcuBd1uw==} + '@amplitude/plugin-page-view-tracking-browser@2.11.14': + resolution: {integrity: sha512-FgdBz7o0XOwfgXFppIj3O+ENeGYhmkQW6LUOEcKQwR1/kKgT+2GhOLD/u9rWnGXYcjYrWH8jtmr7D62OvlF6Dg==} - '@amplitude/plugin-session-replay-browser@1.33.8': - resolution: {integrity: sha512-M91dPju4qEAPpKd0qN22cH8/QbbKlqdFOtvGZGFIBTozEv+8/bekgfpyxBlTdQaR5+NKx/eUtoneM6xIhUWdfA==} + '@amplitude/plugin-session-replay-browser@1.35.0': + resolution: {integrity: sha512-MZJkDaykiCye9QBWqm+fVf5LO+jMxzL1iovdtYxiKe1LTnbcZ8zBFcy0vpWmSfV688bmEfYrmtgzGJQK5m8M6g==} - '@amplitude/plugin-web-vitals-browser@1.1.45': - resolution: {integrity: sha512-Qsi8gpdfgXalHmio/KplZnSjYPkGbvwQUK9rcCW3vGpZo1+jzFSQVlnxVW/WmZ8ncQAtvhDFhd2xTeqLpAfVjQ==} + '@amplitude/plugin-web-vitals-browser@1.1.46': + resolution: {integrity: sha512-Pko9gf7yDNmiEhGq2aJmJmSFckN155AYJWn/3t8NPQXakzWLagFjmOuxvPx4OefIJtyROU1rYmGM/hwTrBSvBQ==} '@amplitude/rrdom@2.1.0': resolution: {integrity: sha512-2dAtxXL02usBV2CSOnScLd3WoVqWaeiGpxN8LuXJ0r/NpLJkW1k876v2tRKAz5NrxPwSdjihsMmwCIXHpJhHfA==} @@ -1700,8 +1697,8 @@ packages: '@amplitude/rrweb@2.1.1': resolution: {integrity: sha512-6uA+5VE/VHumaXPXTTLGRogd/K9MDwd01jGteppeLzsX0PvqlDyY5aIi35yh9+q1iS6ciPBn/2NRg0lg4cFIlw==} - '@amplitude/session-replay-browser@1.48.2': - resolution: {integrity: sha512-N8NRiUlaEXwNhC25guHDCwhtLCZB5G21gZR7qsXJOtwMIebyd4GeM45+xRxo+77rIFIiiFA/Xx59mEXqGrSNrg==} + '@amplitude/session-replay-browser@1.50.0': + resolution: {integrity: sha512-Cp6jcdKNYyIQ6FrJQ2htLd/0SOarpOTvCFMxK799af2i/Q3/VCcgMInXtSLDO3QRHtfM1b+hLmAx3mPqDDSYcA==} '@amplitude/targeting@0.3.10': resolution: {integrity: sha512-z1Vl10M8qHRPs51/cNBWc8HEo1QrAZZtfKzI9Uuim3hCAE/XlPPZzhIwDfZE1qlgFIXLNq1MeNKJTgdDh7A2hg==} @@ -1949,6 +1946,9 @@ packages: '@emnapi/runtime@1.11.2': resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} @@ -2339,160 +2339,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.3': - resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.3': - resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.3': - resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.2': - resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.2': - resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.2': - resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.2': - resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.2': - resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.2': - resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.2': - resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.2': - resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': - resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.2': - resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.3': - resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.3': - resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.3': - resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.3': - resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.3': - resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.3': - resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.3': - resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.3': - resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.3': - resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.3': - resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.3': - resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.3': - resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.3': - resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -2731,8 +2731,8 @@ packages: '@types/react': '>=16' react: '>=16' - '@mediabunny/mp3-encoder@1.55.2': - resolution: {integrity: sha512-T/rNwW/90eF2gaHWr168Z+J030HWDk2DlKB17316v2qbBVfB/EKfD7u6NMvDTFe5TXmK4imAenLPb9S6S4Dr+w==} + '@mediabunny/mp3-encoder@1.55.5': + resolution: {integrity: sha512-WNdWY40KXy6P23iXboIJ8IuLaUsukEF3m+kQ4jCXnDwHTOlyRd+hyq4trTDq7g9kVwB+rpj5ZSzsBq8LKsRYwQ==} peerDependencies: mediabunny: ^1.0.0 @@ -2842,57 +2842,57 @@ packages: '@next/env@16.0.0': resolution: {integrity: sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==} - '@next/env@16.3.3': - resolution: {integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==} + '@next/env@16.3.4': + resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} - '@next/swc-darwin-arm64@16.3.3': - resolution: {integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==} + '@next/swc-darwin-arm64@16.3.4': + resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.3.3': - resolution: {integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==} + '@next/swc-darwin-x64@16.3.4': + resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.3.3': - resolution: {integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==} + '@next/swc-linux-arm64-gnu@16.3.4': + resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.3.3': - resolution: {integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==} + '@next/swc-linux-arm64-musl@16.3.4': + resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.3.3': - resolution: {integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==} + '@next/swc-linux-x64-gnu@16.3.4': + resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.3.3': - resolution: {integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==} + '@next/swc-linux-x64-musl@16.3.4': + resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.3.3': - resolution: {integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==} + '@next/swc-win32-arm64-msvc@16.3.4': + resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.3.3': - resolution: {integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==} + '@next/swc-win32-x64-msvc@16.3.4': + resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2963,8 +2963,8 @@ packages: cpu: [arm] os: [android] - '@oxc-parser/binding-android-arm-eabi@0.143.0': - resolution: {integrity: sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg==} + '@oxc-parser/binding-android-arm-eabi@0.147.0': + resolution: {integrity: sha512-fOtoGvIoirkvxQVw9J1WJPxz571XPgLsPf9uhRD+PJteUnvrJHMDmK9pw2yZEGGyismtRoEsp+JcXUdF/JDMDw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] @@ -2975,8 +2975,8 @@ packages: cpu: [arm64] os: [android] - '@oxc-parser/binding-android-arm64@0.143.0': - resolution: {integrity: sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ==} + '@oxc-parser/binding-android-arm64@0.147.0': + resolution: {integrity: sha512-emjQHOYJaomo4ykaXQ1EItunr/I94Nk01oqBmU4dSkKSTupIDx6OysVDf2e8Eytm77rb+4ZxzgElyWP7rcEX7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -2987,8 +2987,8 @@ packages: cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-arm64@0.143.0': - resolution: {integrity: sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA==} + '@oxc-parser/binding-darwin-arm64@0.147.0': + resolution: {integrity: sha512-kXvBPJL7RmDPJ2mze/vXPPVQimCDtFr9OFLjf7dyhV5Dx64cgcXh9KKrA1sMWvCObvJll9CZZUO0FBlFwD0l6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -2999,8 +2999,8 @@ packages: cpu: [x64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.143.0': - resolution: {integrity: sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ==} + '@oxc-parser/binding-darwin-x64@0.147.0': + resolution: {integrity: sha512-mgFF8pLU6R64LbT27lSrtVRspVC/3IcZ0qyIikzmi78Y3Ik2OPnlAHHI0UEBRcC3qmNgtjaef7zkFt7/uPxIcw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -3011,8 +3011,8 @@ packages: cpu: [x64] os: [freebsd] - '@oxc-parser/binding-freebsd-x64@0.143.0': - resolution: {integrity: sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw==} + '@oxc-parser/binding-freebsd-x64@0.147.0': + resolution: {integrity: sha512-v38aiF11qufOTBcCAKL4skgQf0zJ4NEvRlivq7B5kHrlyvjCLjvNrMtNWDTz1SDUL6/xVsJRmLDxv2e+Cp4oWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -3023,8 +3023,8 @@ packages: cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-gnueabihf@0.143.0': - resolution: {integrity: sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.147.0': + resolution: {integrity: sha512-AeIiBbwUaP0H1+4/qGW9l5qHecS/+XA5iMuieVcGb1T+tyc2dVGspFW13BWk/XrLsiGP/CiDJTJqAPLLCzZHkw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -3035,8 +3035,8 @@ packages: cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.143.0': - resolution: {integrity: sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w==} + '@oxc-parser/binding-linux-arm-musleabihf@0.147.0': + resolution: {integrity: sha512-/41MKPW4RgPY4DJco0NCF0RYX3IMZaVlRNMNzvhaxRavc7tN3Txm+qllZbh0aMRs0VHdgUlbI8TcAOiTai4TKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -3048,8 +3048,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-gnu@0.143.0': - resolution: {integrity: sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA==} + '@oxc-parser/binding-linux-arm64-gnu@0.147.0': + resolution: {integrity: sha512-bmpw/RPhVXgZbtb3xBDuwW5s8+LvZYdqcDSX/sP2ltL77aTio3DP/B5ZTwwgoJ6Mr9vJs4RrmgEKW9XkLNUU1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -3062,8 +3062,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-arm64-musl@0.143.0': - resolution: {integrity: sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ==} + '@oxc-parser/binding-linux-arm64-musl@0.147.0': + resolution: {integrity: sha512-gd7VX/FDVOw6mjQcu45iIcp4QkgybgJwh3a0OFG2NxmPCj628mQWD96QGu1kK8+mZF9qK4b/gIEyC63vQoB7+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -3076,8 +3076,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-ppc64-gnu@0.143.0': - resolution: {integrity: sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw==} + '@oxc-parser/binding-linux-ppc64-gnu@0.147.0': + resolution: {integrity: sha512-HnAzcfki7dSUNHf510Q2NmbJlz8Ys7rn8l9l588Pkx0tYe1BHLZnmELIgqizJ4WPhHGSwN8Ce+B/menVxS3odA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] @@ -3090,8 +3090,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.143.0': - resolution: {integrity: sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg==} + '@oxc-parser/binding-linux-riscv64-gnu@0.147.0': + resolution: {integrity: sha512-qlkOL6wT44U+fT5s/+sR6Shx0OdwvQF83JyIPZUxG/ovqZF5/7atOtjH+JPZ5/7ATQLbFBBSmghcy/+2NVB/ew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] @@ -3104,8 +3104,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-riscv64-musl@0.143.0': - resolution: {integrity: sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g==} + '@oxc-parser/binding-linux-riscv64-musl@0.147.0': + resolution: {integrity: sha512-DlefD7L7sMXs/3hIBH23Egk0phj8kG0SA81dVGOQ3S1ekjOlmTLH2E+F2Thwfh1slKx6aH+lNc5fQYAw0GU7/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] @@ -3118,8 +3118,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-s390x-gnu@0.143.0': - resolution: {integrity: sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ==} + '@oxc-parser/binding-linux-s390x-gnu@0.147.0': + resolution: {integrity: sha512-Xqpagk/031IvZ4svrk2FF01YEqM/iN3MJV3SVZadKg/CsGlDCGoREqKHXYnoV5+8SfGe/m6RM1szXFLusTu/Uw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -3132,8 +3132,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.143.0': - resolution: {integrity: sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw==} + '@oxc-parser/binding-linux-x64-gnu@0.147.0': + resolution: {integrity: sha512-QioQOeUbI4ATUr0S2z88uA3Cds2R3Mm5Ge7U8XNYtlTb2GJF3rWlcj70z0AJhhOlbdm0YgVjqPBldUNbFylDIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -3146,8 +3146,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-x64-musl@0.143.0': - resolution: {integrity: sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ==} + '@oxc-parser/binding-linux-x64-musl@0.147.0': + resolution: {integrity: sha512-NXy1tv/OdC+pPTwf9RiCZWPK53V/Xq/2cjSnjOSyKopajdaDqMIkgtDY+jXZemp2e8px5FeWfY2L2LwhKZQovg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -3159,8 +3159,8 @@ packages: cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-openharmony-arm64@0.143.0': - resolution: {integrity: sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw==} + '@oxc-parser/binding-openharmony-arm64@0.147.0': + resolution: {integrity: sha512-GpGWZ6oKz4bjCWW9Mz5pCaGPyk2Aaze6zEoaslIQqpSLtpx5pXj/ap5gUNb5Jn2LIbqWyjkGLX9yv3NMuNcBVQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -3176,8 +3176,8 @@ packages: cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-arm64-msvc@0.143.0': - resolution: {integrity: sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ==} + '@oxc-parser/binding-win32-arm64-msvc@0.147.0': + resolution: {integrity: sha512-a8mlt7CC8z7LUdCfaxhff4kCd+vSjE+NEFL0cxA8ukfuSnvAto/pWTjytW4BuVLnQGcCVdHJwcRKOfs++H+tjw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -3188,8 +3188,8 @@ packages: cpu: [ia32] os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.143.0': - resolution: {integrity: sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA==} + '@oxc-parser/binding-win32-ia32-msvc@0.147.0': + resolution: {integrity: sha512-M5ViVDBcFLnl2632AuuWuP35zEL5oikK1jTx8r3+902VEDeSNHxFZAB6RZyfZ7MU6Oi5QTOG8MmMkIeHsudSaw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] @@ -3200,8 +3200,8 @@ packages: cpu: [x64] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.143.0': - resolution: {integrity: sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw==} + '@oxc-parser/binding-win32-x64-msvc@0.147.0': + resolution: {integrity: sha512-DUaE13OwnUSlHpLZNcC/nuT10ivlWqc5EZgsfgXuAmWYw0r3nDxGeLD1zlGwYwIgVk1/ZMAxoXpV+05stvbHaA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3213,12 +3213,12 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - '@oxc-project/types@0.143.0': - resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} - '@oxc-project/types@0.146.0': resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} + '@oxc-project/types@0.147.0': + resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} + '@oxc-resolver/binding-android-arm-eabi@11.21.2': resolution: {integrity: sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==} cpu: [arm] @@ -3774,71 +3774,71 @@ packages: resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.62.5 + rollup: 4.63.1 peerDependenciesMeta: rollup: optional: true - '@sentry/browser-utils@10.70.0': - resolution: {integrity: sha512-IvjhafF5NFXrPCg5EHbAPGDwIhHxgUcLlHTklZ3DAdA6ky88BJYMfevbcnOEmgavf4nylhCaquRUFNB5+szj+A==} + '@sentry/browser-utils@10.73.0': + resolution: {integrity: sha512-qQygxJZ+RV779+iL1+lrJ4f4sZLgbgW0/JWPNp0YlcEAE62yCsdKbqoTEjB/EugdS4mSjBMX0chZC6rblu2Ycw==} engines: {node: '>=18'} - '@sentry/browser@10.70.0': - resolution: {integrity: sha512-IK6+J+8H06tZe+A8L37TT5ZxxwNtyQatW8zl5RYYJ/e9CsjrM8fPi8I1OT7uquTw8UtjqFHt7bEef/Vy63ksPg==} + '@sentry/browser@10.73.0': + resolution: {integrity: sha512-HqTe1S5RrWLufhX2LaFP3yNoMxfNDroh120bq1zdGHZfFDBMJQ0CDXxHO+L4UJfQ5dWdCCzWbXIAiZuWGa/DFQ==} engines: {node: '>=18'} '@sentry/conventions@0.16.0': resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} engines: {node: '>=14'} - '@sentry/core@10.70.0': - resolution: {integrity: sha512-ozhCTDqg89oB4XmWfAwuHshABpvT7AkRpaPnogopPfMAaI61G1t8EKCJ4W7aum8JSBonlfyjPCyW5oYZFm0KvA==} + '@sentry/core@10.73.0': + resolution: {integrity: sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA==} engines: {node: '>=18'} - '@sentry/feedback@10.70.0': - resolution: {integrity: sha512-6VQn2ETJjHkk4QQDdx/587/JDfXsk3yBTLZ3UZOMtBVrkmwgk+1FJZ8ULJ3ud1xZcuh2icunIkc7tGVv2axdnw==} + '@sentry/feedback@10.73.0': + resolution: {integrity: sha512-D6nSngX+e46Mae2/oh2bxBvxNK1z2NERbuMAhB5sx9x4xMBWIyGnYYTECehvEqV9+AqGAgxxhOZoYIG3AmRwww==} engines: {node: '>=18'} - '@sentry/react@10.70.0': - resolution: {integrity: sha512-j1d/4hvoaUVKs5GRrfmwUCkiEmkfaeHsk+xgausZlzg5YvmwpF+gyevBLNP26GFEH7nyHXW4l8dbbo0eNieJmw==} + '@sentry/react@10.73.0': + resolution: {integrity: sha512-wJrzS98ddPvhGS/MKNHZyE8X7ecd4KwKdz7fHino2qkqBBrF4cxWr+q/uLTIk5PYWMkeJ4oKMjHAjOqmzGR4tg==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - '@sentry/replay-canvas@10.70.0': - resolution: {integrity: sha512-irzpw22bK5CF3jbecDa0gBUcfjv7tgeUoLAvtIfeHOP5ajmf3o4Cp99a9RQqkfgvkcMeRZBUwE91SQhp6Ank+w==} + '@sentry/replay-canvas@10.73.0': + resolution: {integrity: sha512-sxa2lKkHPfF/j5xFpW7gocthWXRqyoHz8KCPy6yGc8plT477nl57iGSKhQDYsx1Ny10TLjs7YkIuPP1GY/Ax2Q==} engines: {node: '>=18'} - '@sentry/replay@10.70.0': - resolution: {integrity: sha512-xMnSGzJn9Xd29rYd32lkx/gFW+5mtgqADJ2FiZvis0MBGZuDlNRwPn0/Cs1xA3JNXKV3NGfhdmmvs90w4dHSmw==} + '@sentry/replay@10.73.0': + resolution: {integrity: sha512-nN2wjN/Y0J5BOJV5hqRHUEBfxwUsipp1PKjcDHh6Fpxnrtfldu3Y99E8cQInseo5heFdzEvrOHBBrqWZXOHVKQ==} engines: {node: '>=18'} - '@shikijs/core@4.4.2': - resolution: {integrity: sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==} + '@shikijs/core@4.4.3': + resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.4.2': - resolution: {integrity: sha512-MnIkeqWdVPUWsxlx8gKLVCJFTsqrQJgpTPBPpQwaFeJ56lOnJxj5aN2LUFnfxUEcvOQuNocmbaMnVrCEln6rkw==} + '@shikijs/engine-javascript@4.4.3': + resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.4.2': - resolution: {integrity: sha512-GLhowz1+jixjz+wiZ3wMnOn1jTxiFCGl2PkXufivbnwPHKuyw1AYqu5/hbWhZZ2oAb0NP05WUJhYeigY14drnw==} + '@shikijs/engine-oniguruma@4.4.3': + resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==} engines: {node: '>=20'} - '@shikijs/langs@4.4.2': - resolution: {integrity: sha512-8DfeusD+Zdv/eYIDdXyJTUnSMHt+aAWjAOCXV20HNGAHRlInXpG8wh421v6B91WOm9TFwRLN+b/LG5F2NAIojg==} + '@shikijs/langs@4.4.3': + resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==} engines: {node: '>=20'} - '@shikijs/primitive@4.4.2': - resolution: {integrity: sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==} + '@shikijs/primitive@4.4.3': + resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} engines: {node: '>=20'} - '@shikijs/themes@4.4.2': - resolution: {integrity: sha512-H0CFoL07ddDC2Dd6EdrPYNkRhUR6YCkJlnuYFceYYUJJA5TIm2b5B33qqiDYryBExgbKMndFJPb2u1gTuqO37g==} + '@shikijs/themes@4.4.3': + resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==} engines: {node: '>=20'} - '@shikijs/types@4.4.2': - resolution: {integrity: sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==} + '@shikijs/types@4.4.3': + resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': @@ -3923,7 +3923,7 @@ packages: resolution: {integrity: sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw==} peerDependencies: esbuild: ^0.28.2 - rollup: 4.62.5 + rollup: 4.63.1 storybook: ^10.5.10 vite: '*' webpack: '*' @@ -4154,8 +4154,8 @@ packages: engines: {node: '>=18'} hasBin: true - '@tanstack/eslint-plugin-query@5.102.2': - resolution: {integrity: sha512-0uRcRXvrZN3JLtGDMXU8Qp/or/fp1VhDZEVcD8WrUc7CF0uWbJbloJ88dfFDhiocYP31wXX4Buar/WC31fS5Ug==} + '@tanstack/eslint-plugin-query@5.102.8': + resolution: {integrity: sha512-zRjG2PL3zvoqnsSNJFyfX5Mo+OwRwTbLERwZVO3oNQQn82V949INwaLrprzsOW/ivuow7gASeAUUC24xH7kwSQ==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ^5.6.0 || ^6.0.0 || ^7.0.0 @@ -4174,8 +4174,8 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.102.2': - resolution: {integrity: sha512-zQ5794PXBlV5Wl7N23SR1/Ss+wPE/h2Ye4XlKpm3omN8i8b/Fd4DAdqpLS2AXj5M/RMObt3k5qy3E7kzt/AvBA==} + '@tanstack/query-core@5.102.8': + resolution: {integrity: sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==} '@tanstack/react-form@1.33.5': resolution: {integrity: sha512-LlRB28qJwO/QCGaHvWnbdh4haBgTFiZVmzA2uzxSBS3YA7/IqrQ6HOBK70CkFQ+DbflZ7NawsmSln13h5iIdTA==} @@ -4193,8 +4193,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-query@5.102.2': - resolution: {integrity: sha512-KxU8ZyOEuJ81eTSgXa8GQbk/jO/rz0elYtNKt3VMtM2pRjeO8ADIu7sqqmEjFKJKqco9h2N1ojIDuHs6VfDItQ==} + '@tanstack/react-query@5.102.8': + resolution: {integrity: sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==} peerDependencies: react: ^18 || ^19 @@ -4228,8 +4228,8 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -4458,6 +4458,9 @@ packages: '@types/node@26.0.1': resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==} + '@types/node@26.4.0': + resolution: {integrity: sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -4502,55 +4505,55 @@ packages: '@types/zen-observable@0.8.3': resolution: {integrity: sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==} - '@typescript-eslint/parser@8.67.0': - resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==} + '@typescript-eslint/parser@8.69.0': + resolution: {integrity: sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.67.0': - resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==} + '@typescript-eslint/project-service@8.69.0': + resolution: {integrity: sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.67.0': - resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==} + '@typescript-eslint/scope-manager@8.69.0': + resolution: {integrity: sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.67.0': - resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==} + '@typescript-eslint/tsconfig-utils@8.69.0': + resolution: {integrity: sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.67.0': - resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==} + '@typescript-eslint/type-utils@8.69.0': + resolution: {integrity: sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.67.0': - resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==} + '@typescript-eslint/types@8.69.0': + resolution: {integrity: sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.67.0': - resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==} + '@typescript-eslint/typescript-estree@8.69.0': + resolution: {integrity: sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.67.0': - resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==} + '@typescript-eslint/utils@8.69.0': + resolution: {integrity: sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.67.0': - resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} + '@typescript-eslint/visitor-keys@8.69.0': + resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/typescript-aix-ppc64@7.0.2': @@ -4714,8 +4717,8 @@ packages: peerDependencies: vite: '*' - '@vitejs/plugin-react@6.1.0': - resolution: {integrity: sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==} + '@vitejs/plugin-react@6.1.1': + resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -5196,6 +5199,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + baseline-browser-mapping@2.11.20: + resolution: {integrity: sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==} + engines: {node: '>=6.0.0'} + hasBin: true + birecord@0.1.2: resolution: {integrity: sha512-5PAPTTmMpMEb+GuMb5DebfBkipRGyIW9+gtwEBSoDA9xkhHILm04+hZQ702pMksu3d8YAuGkmgTzQWcKqTPScA==} @@ -5221,6 +5229,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -5284,6 +5297,9 @@ packages: caniuse-lite@1.0.30001799: resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + canvas@3.2.3: resolution: {integrity: sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==} engines: {node: ^18.12.0 || >= 20.9.0} @@ -5433,6 +5449,10 @@ packages: resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==} engines: {node: '>= 12.0.0'} + comment-parser@1.4.8: + resolution: {integrity: sha512-rKZTGo4fzKYna8UcL0isTg5wkBNla7bxTypLwZQXjIdi++IdP1OJ41rI5Mti3/jltkPujbu4i9LIARYA+zpotQ==} + engines: {node: '>= 12.0.0'} + compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -5461,8 +5481,9 @@ packages: copy-to-clipboard@4.0.2: resolution: {integrity: sha512-gklSft7IuhriZKHKpuoA1fpJSLPNgvUMWMo5BlnzAJm0zNKnznoSv23IjtNqclx8eKi6ZcdvFFzYEER/+U1LoQ==} - core-js-compat@3.49.0: - resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + core-js-compat@3.50.0: + resolution: {integrity: sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==} + engines: {node: '>=6.4.0'} cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -5729,6 +5750,10 @@ packages: resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} engines: {node: '>=18'} + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} + engines: {node: '>=18'} + define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -5819,6 +5844,9 @@ packages: electron-to-chromium@1.5.380: resolution: {integrity: sha512-W6d5AbuEoRayO447cqrg6lKJIlscgRnnxOZl/08kfV71BQDoEBC7Wwis68z87LjyK6f4kWyTaubuDbhHKrZkbA==} + electron-to-chromium@1.5.417: + resolution: {integrity: sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ==} + elkjs@0.11.1: resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} @@ -5912,6 +5940,9 @@ packages: es-toolkit@1.51.0: resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} + es-toolkit@1.52.0: + resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==} + esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} @@ -6034,8 +6065,8 @@ packages: peerDependencies: eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 - eslint-plugin-perfectionist@5.10.1: - resolution: {integrity: sha512-Kprsp9Us0GqAesYaAIzUViw57xYp5WBqzXrcE0Mtww++E5fexWXYBipMuuD7yvyH4vvpBH0+oJ+OMAmZ0oYXkw==} + eslint-plugin-perfectionist@5.11.0: + resolution: {integrity: sha512-kZV1otBcu4xT5R1p+0x1N1wRv4pS+OIbxrA47n5a1fTnN3MWbexOx5eQgbQhGyCNbpvF/rqrbnpkGjumHF+Y0Q==} engines: {node: ^20.0.0 || >=22.0.0} peerDependencies: eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 @@ -6128,8 +6159,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.9.0: - resolution: {integrity: sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw==} + eslint@10.9.1: + resolution: {integrity: sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -6227,7 +6258,7 @@ packages: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} peerDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 peerDependenciesMeta: picomatch: optional: true @@ -6259,20 +6290,20 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} - formatly@0.3.0: - resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + formatly@0.7.0: + resolution: {integrity: sha512-7CXJtIIA0zy/u12StsYk25qVKxvdLA2ep2sTNxK3ov0mGNIIDqIvAXDSgTnAfDJFsPfWjuz0WjfYSdpvnLA5Tg==} engines: {node: '>=18.3.0'} hasBin: true - foxact@0.3.9: - resolution: {integrity: sha512-uxOL+WzfsUAqkhBR44+om67yslYTHM9ymBsIZpr4EbPEV13oDL1XtFJQOUInci2nHmqBd3aolSC+NthQIsFbUQ==} + foxact@0.3.10: + resolution: {integrity: sha512-5pyt7M2ngc9PyGX5soP3q/L6Z+JkNPEeGTyc5t200szvEiGofWy41bxNGUE/jpuhIDByLLyuumbc712HCdJWTg==} peerDependencies: react: '*' react-dom: '*' @@ -6336,8 +6367,8 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - get-tsconfig@4.14.1: - resolution: {integrity: sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==} + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} giget@3.3.0: resolution: {integrity: sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==} @@ -6369,8 +6400,8 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} - globals@17.7.0: - resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + globals@17.11.0: + resolution: {integrity: sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==} engines: {node: '>=18'} globrex@0.1.2: @@ -6392,8 +6423,8 @@ packages: hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} - happy-dom@20.11.6: - resolution: {integrity: sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg==} + happy-dom@20.12.0: + resolution: {integrity: sha512-7uMYJu2SEwwL8vVcKp0C0lnt6d2LSGGe+T+oY79PiCJNNSgFpbxW8n5KuzpDQvrU4mt+fYiK1+Jy7Z2v39YR6g==} engines: {node: '>=20.0.0'} has-ansi@6.0.2: @@ -6450,8 +6481,8 @@ packages: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} - hono@4.13.3: - resolution: {integrity: sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==} + hono@4.13.5: + resolution: {integrity: sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==} engines: {node: '>=16.9.0'} hosted-git-info@9.0.3: @@ -6511,8 +6542,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.8: + resolution: {integrity: sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==} engines: {node: '>= 4'} image-size@2.0.2: @@ -6679,8 +6710,8 @@ packages: react: optional: true - jotai@2.20.2: - resolution: {integrity: sha512-aHB4CNb9qRcyf0mwSB6EO5bCGAjx8cTwFgOFCE2leOnTzqACbnSWG8XoWB3LxCT1Qoj03I1OWAHszDmN4uHb/w==} + jotai@2.20.3: + resolution: {integrity: sha512-N8L9FUbeGDP+0qNJw1FebOxt+5hk+jTOwUA2LlRE4C081jUcY6F1T/FSETp4DT2/INMtiSRZyNm2D+yZdzrSgA==} engines: {node: '>=12.20.0'} peerDependencies: '@babel/core': ^7.29.1 @@ -6716,14 +6747,18 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true - js-yaml@5.3.0: - resolution: {integrity: sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==} + js-yaml@5.4.1: + resolution: {integrity: sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==} hasBin: true jsdoc-type-pratt-parser@7.2.0: resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} engines: {node: '>=20.0.0'} + jsdoc-type-pratt-parser@7.3.0: + resolution: {integrity: sha512-DoyJXo7x/n48M3NsGOs9QnEws0ft0tV3YsSgvWMNxz2hZtz+Q6fpqUD96lVYX4a+jcEkzHeFNDJOn68TzXfbdA==} + engines: {node: '>=20.0.0'} + jsdoc-type-pratt-parser@8.0.0: resolution: {integrity: sha512-uQu/fXVqVaMg6gM8/E5G5+eygVcZ1NV0Z51CvqhNa2bDWxvHMl484ETr6vph4oPyC+KUcbP/w2W2pewfCiR9aQ==} engines: {node: '>=20.0.0'} @@ -6774,8 +6809,8 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - knip@6.32.2: - resolution: {integrity: sha512-WXTXbmocrw7gqm1A1TQvFN0OgJ7hUSU6E1g6SPRIzzHFogUBhXByc7cYeOFVtJ2uODg7DP4VbESYBYnfbtBYsg==} + knip@6.34.0: + resolution: {integrity: sha512-bbHIrnGspYwe4EBPjjx+lvkUor0F2qfKQc5BPzPI4SOAImYA+k2ueVpIcF7d/W1LEVT7XoJUPt6zELeGZhXBgA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -6785,8 +6820,8 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - ky@2.0.2: - resolution: {integrity: sha512-/GmXpo9F9W+f8n4Ivr2iH+7h7wL7jLbLKWkMlpflcCRb6kGjBfTlASEXaZ9qUgNTn4VgS0P2pwxxzQ4EM6Ulgg==} + ky@2.1.0: + resolution: {integrity: sha512-nIKwelw+7qVqKOlX4Eht6GXfEXlDHcdIjyLWV3s119kYt5s+70ayTnYS+0fRNmtgGRRAfqdeWQbh8YizqkS0ng==} engines: {node: '>=22'} launch-ide@1.4.5: @@ -7004,8 +7039,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loro-crdt@1.14.1: - resolution: {integrity: sha512-1BQ7neoQeJdCbD8gxlLHsy8tqCLYIJGrXSZic4s3t7toIsiFfks8Z0nGWvuEWr5miZ7giJx19x5ffkBaw+g9KQ==} + loro-crdt@1.15.1: + resolution: {integrity: sha512-J3568cXG75MNotTJg2XVhlIEW8POjAZRFhK+3yXYBaQVB1LHn/Ksk3DIqoTOBUf+cgE4yVeqPbh7VUTX2QLV2g==} loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -7118,15 +7153,15 @@ packages: mdn-data@2.29.0: resolution: {integrity: sha512-pVxQFCcaYUEAH853+v7yoI/qzhxXSq1bTb9obMYGYAN1c3Hen+XDCEvr296XhstrwlSTNgOR7mCSD4JPjbJe5A==} - mediabunny@1.55.2: - resolution: {integrity: sha512-EEx4O6qYddAdCyWPMZNDwI7uc5hewNHrPAf9jLcVhIbXoPsiqNQ+D9i1pfadmGkjN2V318jSrZljkpoziYm6Lg==} + mediabunny@1.55.5: + resolution: {integrity: sha512-m0v6y8FGXiK+HKOc3AZqU+kJPLYsSKaLitmQNQIIHZKIGlTwQ34OF+X6Ul0K8iV4b03oPse10apwuoqbvmKeAA==} merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.17.0: - resolution: {integrity: sha512-Jo9N377Wb4MSnHFPTbLi2SxFpsQl4eVHoxnW5U1Md9EazvgMp3s+4ohDxr81YNTgbn5Kj7HJ3yslrSJ52kwpbA==} + mermaid@11.17.2: + resolution: {integrity: sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -7327,8 +7362,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.3.3: - resolution: {integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==} + next@16.3.4: + resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -7359,7 +7394,11 @@ packages: resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} - node@runtime:22.23.2: + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + engines: {node: '>=18'} + + node@runtime:24.20.0: resolution: type: variations variants: @@ -7367,9 +7406,9 @@ packages: archive: tarball bin: node: bin/node - integrity: sha256-d8YwMv1KuOmOCufbnXnjlmWeBK+Vpe344cqGnkJsBKU= + integrity: sha256-FLW9etQKtfRxX7Ti+WSFjYINlCX+KR09ZfmJJua4nB4= type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-aix-ppc64.tar.gz + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-aix-ppc64.tar.gz targets: - cpu: ppc64 os: aix @@ -7377,9 +7416,9 @@ packages: archive: tarball bin: node: bin/node - integrity: sha256-YRMPOUwWMNIR3VCuzENT03lIDzbTrJE82F27oa7VhcY= + integrity: sha256-QOVgfl7LPbkZJyN3baLXXZZiYPx0p6nnMcG9Z92pa8g= type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-darwin-arm64.tar.gz + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-darwin-arm64.tar.gz targets: - cpu: arm64 os: darwin @@ -7387,9 +7426,9 @@ packages: archive: tarball bin: node: bin/node - integrity: sha256-WOmQIsL/iTlVdsx/1NmM6iS7aAgUddX4i4Ae6HKfsCY= + integrity: sha256-nlsmRM8Qe++2rvymdrltMpa8EBOAlvAi7TeNYjPtgfQ= type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-darwin-x64.tar.gz + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-darwin-x64.tar.gz targets: - cpu: x64 os: darwin @@ -7397,9 +7436,9 @@ packages: archive: tarball bin: node: bin/node - integrity: sha256-ATtZz9KBlwOm9KFKuJH8RvwqTj9bzZLeP7SSm0PjWzA= + integrity: sha256-NRVgPiSHh5o5vHVxbxoq/9AnUAxkulDoRc9yyzMhkBM= type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-linux-arm64.tar.gz + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-arm64.tar.gz targets: - cpu: arm64 os: linux @@ -7407,19 +7446,9 @@ packages: archive: tarball bin: node: bin/node - integrity: sha256-Ki9Z64/Z3sJ7O+4XxykTHR/T5tmUPUefEVbOOK+M1Zk= + integrity: sha256-pzSzbI0dFs5FXA65wymwfdEhwshVQ1UGSphhvJbDrcw= type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-linux-armv7l.tar.gz - targets: - - cpu: armv7l - os: linux - - resolution: - archive: tarball - bin: - node: bin/node - integrity: sha256-ZciqnmRxlvMNNWS7ayATBrG1IPcvDdMnjaMmAUoBg3k= - type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-linux-ppc64le.tar.gz + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-ppc64le.tar.gz targets: - cpu: ppc64le os: linux @@ -7427,9 +7456,9 @@ packages: archive: tarball bin: node: bin/node - integrity: sha256-mGuEQbHRoBxw7XELqyAXQf0NGW0wKn5DOP79XhwQ5h4= + integrity: sha256-2MIktG7wHweKUj2bfbeSgjBvrigemGoVotv/6UfFMB4= type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-linux-s390x.tar.gz + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-s390x.tar.gz targets: - cpu: s390x os: linux @@ -7437,9 +7466,20 @@ packages: archive: tarball bin: node: bin/node - integrity: sha256-spSlVuY51kM4gjkg5YZsIcAnQXQtLhUp7hoiXB7JJSo= + integrity: sha256-1ubRu7mtSssW1YC4m+ipyafkkJJuk708MKINRuFSv4o= type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-linux-x64.tar.gz + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-x64-musl.tar.gz + targets: + - cpu: x64 + os: linux + libc: musl + - resolution: + archive: tarball + bin: + node: bin/node + integrity: sha256-hV1YH4pOsagRfjQm3iX+AncFkv68+zE2mu4f+/7p6Ow= + type: binary + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-x64.tar.gz targets: - cpu: x64 os: linux @@ -7447,10 +7487,10 @@ packages: archive: zip bin: node: node.exe - integrity: sha256-/sAlptoxdX47avhMWhYo6dOEQsqZohYQkdePL8+jXvM= - prefix: node-v22.23.2-win-arm64 + integrity: sha256-McZ5l0TeilRgFkMJgEDGjDaX5WyU5AfWHQ5fpfNBkdc= + prefix: node-v24.20.0-win-arm64 type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-win-arm64.zip + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-win-arm64.zip targets: - cpu: arm64 os: win32 @@ -7458,31 +7498,20 @@ packages: archive: zip bin: node: node.exe - integrity: sha256-EXe0E3ulrapWNUrkDxCAx0UOiuCc7LR9pFnRxSrJn5c= - prefix: node-v22.23.2-win-x64 + integrity: sha256-bKyf+8qPakcJHktcdy4GBgScOHHLZ9kAwM7d5jDlRbo= + prefix: node-v24.20.0-win-x64 type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-win-x64.zip + url: https://nodejs.org/download/release/v24.20.0/node-v24.20.0-win-x64.zip targets: - cpu: x64 os: win32 - - resolution: - archive: zip - bin: - node: node.exe - integrity: sha256-clyeK90cIBa0HJlagfT6Ns5OLuVlt0Vdj4iRgnJ99kc= - prefix: node-v22.23.2-win-x86 - type: binary - url: https://nodejs.org/download/release/v22.23.2/node-v22.23.2-win-x86.zip - targets: - - cpu: x86 - os: win32 - resolution: archive: tarball bin: node: bin/node - integrity: sha256-t6GiscfHbkdVDxd2RnaTmEDgpktNBL3TdaSsFLzKqNg= + integrity: sha256-LIxQfMsPIIEtlSa6jKRUsWUqre9o/IutBvB/sRIt0e8= type: binary - url: https://unofficial-builds.nodejs.org/download/release/v22.23.2/node-v22.23.2-linux-arm64-musl.tar.gz + url: https://unofficial-builds.nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-arm64-musl.tar.gz targets: - cpu: arm64 os: linux @@ -7491,14 +7520,14 @@ packages: archive: tarball bin: node: bin/node - integrity: sha256-OW4R7mCesuXLmQ8EXE0DeqR7LCR/POywHFwWLjP/qa8= + integrity: sha256-muE5n+9L2JkOFXc84TJ7M2oguel9jHVJ9PQspzxD9WI= type: binary - url: https://unofficial-builds.nodejs.org/download/release/v22.23.2/node-v22.23.2-linux-x64-musl.tar.gz + url: https://unofficial-builds.nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-x64-musl.tar.gz targets: - cpu: x64 os: linux libc: musl - version: 22.23.2 + version: 24.20.0 hasBin: true normalize-package-data@8.0.0: @@ -7514,8 +7543,8 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nuqs@2.10.0: - resolution: {integrity: sha512-uy62jWCXiU1mcGfoClUFNTfrbaxfPQ8WuiomYlkXw5llVdN8kRZVYxOQ2LD70k3DzVQz5bZczE9ClVXwOC22Dw==} + nuqs@2.10.1: + resolution: {integrity: sha512-7lPZrPJVOsD0VvfQSKtodYBBPGPLsLzkFU8ueYIap9yMMJvSw6UC12p8luA5NW7aKe7AbPHKxcxb3OluH6iAJA==} peerDependencies: '@remix-run/react': '>=2' '@tanstack/react-router': ^1 @@ -7574,6 +7603,10 @@ packages: resolution: {integrity: sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==} engines: {node: '>=20'} + open@11.0.2: + resolution: {integrity: sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q==} + engines: {node: '>=20'} + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -7589,8 +7622,8 @@ packages: resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} engines: {node: ^20.19.0 || >=22.12.0} - oxc-parser@0.143.0: - resolution: {integrity: sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA==} + oxc-parser@0.147.0: + resolution: {integrity: sha512-5xaug6t7GfV3BO5Iv+xHW1rmQkDEQ3BEu3L8g3InsvWO5i8CYGc4tCZ2X985QcwWNycFJam+aOns6Nr2XAThTA==} engines: {node: ^20.19.0 || >=22.12.0} oxc-resolver@11.21.2: @@ -7648,6 +7681,9 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + pad-right@0.2.2: resolution: {integrity: sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==} engines: {node: '>=0.10.0'} @@ -7739,6 +7775,10 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pinyin-pro@3.29.3: resolution: {integrity: sha512-+UU9bx6vfDw8amOJGHm0TE0rdQl8VPylsDWviQ5OOQ3e+on1xRP4OqDbiDuMT5OISgvfl/Y6ez1BBRaIP80GLQ==} @@ -7798,6 +7838,10 @@ packages: resolution: {integrity: sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==} engines: {node: '>=20'} + powershell-utils@0.2.1: + resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==} + engines: {node: '>=20'} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -7833,8 +7877,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} quansync@0.2.11: @@ -8048,8 +8092,8 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - remend@1.3.0: - resolution: {integrity: sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==} + remend@1.3.1: + resolution: {integrity: sha512-N3DiY5qbRPoa5vkxn1oDLMyXOVTeo6Hp+XOj6SIqJAYUgLS0Q587gILPMom/qm86AQ/ZrcOdwEIzCz8V3J0nxQ==} repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} @@ -8148,8 +8192,8 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} - sharp@0.35.3: - resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' @@ -8169,8 +8213,8 @@ packages: resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} engines: {node: '>= 0.4'} - shiki@4.4.2: - resolution: {integrity: sha512-P8F/dFhRevaw2uSdeIYlq/5SXZNY85DPtmXQ947gD1Zj2JqO5AkNvVVBar0Me9JkFx3uzVud/qOtP5ek9NEQGA==} + shiki@4.4.3: + resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} engines: {node: '>=20'} siginfo@2.0.0: @@ -8196,8 +8240,8 @@ packages: size-sensor@1.0.3: resolution: {integrity: sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==} - smol-toml@1.7.1: - resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==} + smol-toml@1.8.0: + resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==} engines: {node: '>= 18'} socket.io-client@4.8.3: @@ -8285,8 +8329,8 @@ packages: vite-plus: optional: true - streamdown@2.5.0: - resolution: {integrity: sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==} + streamdown@2.6.0: + resolution: {integrity: sha512-nQZVUn4GvB2R5SAlDNph20iKZeW+RM4gv6G1H3ACypEnmQf8PgTHZ/1Ta2OACRwQ2coK7aW5AeIAa7Ls5zk+RA==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -8472,11 +8516,11 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - tldts-core@7.4.10: - resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} + tldts-core@7.4.11: + resolution: {integrity: sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==} - tldts@7.4.10: - resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} + tldts@7.4.11: + resolution: {integrity: sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==} hasBin: true to-regex-range@5.0.1: @@ -8552,8 +8596,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.23.12: - resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} engines: {node: '>=18.0.0'} hasBin: true @@ -8663,6 +8707,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -9023,6 +9073,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.5.4: + resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==} + zrender@6.1.0: resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==} @@ -9073,28 +9126,21 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@amplitude/analytics-browser@2.45.6': + '@amplitude/analytics-browser@2.45.8': dependencies: - '@amplitude/analytics-core': 2.54.2 - '@amplitude/plugin-autocapture-browser': 1.28.11 - '@amplitude/plugin-custom-enrichment-browser': 0.1.21 - '@amplitude/plugin-event-property-attribution-browser': 0.2.13 - '@amplitude/plugin-network-capture-browser': 1.10.13 - '@amplitude/plugin-page-url-enrichment-browser': 0.7.23 - '@amplitude/plugin-page-view-tracking-browser': 2.11.13 - '@amplitude/plugin-web-vitals-browser': 1.1.45 - tslib: 2.8.1 - - '@amplitude/analytics-client-common@2.4.60': - dependencies: - '@amplitude/analytics-connector': 1.6.4 - '@amplitude/analytics-core': 2.54.2 - '@amplitude/analytics-types': 2.11.1 + '@amplitude/analytics-core': 2.55.0 + '@amplitude/plugin-autocapture-browser': 1.29.0 + '@amplitude/plugin-custom-enrichment-browser': 0.1.22 + '@amplitude/plugin-event-property-attribution-browser': 0.2.14 + '@amplitude/plugin-network-capture-browser': 1.10.14 + '@amplitude/plugin-page-url-enrichment-browser': 0.7.24 + '@amplitude/plugin-page-view-tracking-browser': 2.11.14 + '@amplitude/plugin-web-vitals-browser': 1.1.46 tslib: 2.8.1 '@amplitude/analytics-connector@1.6.4': {} - '@amplitude/analytics-core@2.54.2': + '@amplitude/analytics-core@2.55.0': dependencies: '@amplitude/analytics-connector': 1.6.4 '@types/zen-observable': 0.8.3 @@ -9104,7 +9150,7 @@ snapshots: '@amplitude/analytics-types@2.11.1': {} - '@amplitude/element-selector@0.2.1': + '@amplitude/element-selector@0.3.0': dependencies: tslib: 2.8.1 @@ -9112,52 +9158,51 @@ snapshots: dependencies: js-base64: 3.8.0 - '@amplitude/plugin-autocapture-browser@1.28.11': + '@amplitude/plugin-autocapture-browser@1.29.0': dependencies: - '@amplitude/analytics-core': 2.54.2 - '@amplitude/element-selector': 0.2.1 + '@amplitude/analytics-core': 2.55.0 + '@amplitude/element-selector': 0.3.0 tslib: 2.8.1 - '@amplitude/plugin-custom-enrichment-browser@0.1.21': + '@amplitude/plugin-custom-enrichment-browser@0.1.22': dependencies: - '@amplitude/analytics-core': 2.54.2 + '@amplitude/analytics-core': 2.55.0 tslib: 2.8.1 - '@amplitude/plugin-event-property-attribution-browser@0.2.13': + '@amplitude/plugin-event-property-attribution-browser@0.2.14': dependencies: - '@amplitude/analytics-core': 2.54.2 + '@amplitude/analytics-core': 2.55.0 tslib: 2.8.1 - '@amplitude/plugin-network-capture-browser@1.10.13': + '@amplitude/plugin-network-capture-browser@1.10.14': dependencies: - '@amplitude/analytics-core': 2.54.2 + '@amplitude/analytics-core': 2.55.0 tslib: 2.8.1 - '@amplitude/plugin-page-url-enrichment-browser@0.7.23': + '@amplitude/plugin-page-url-enrichment-browser@0.7.24': dependencies: - '@amplitude/analytics-core': 2.54.2 + '@amplitude/analytics-core': 2.55.0 tslib: 2.8.1 - '@amplitude/plugin-page-view-tracking-browser@2.11.13': + '@amplitude/plugin-page-view-tracking-browser@2.11.14': dependencies: - '@amplitude/analytics-core': 2.54.2 + '@amplitude/analytics-core': 2.55.0 tslib: 2.8.1 - '@amplitude/plugin-session-replay-browser@1.33.8(@amplitude/rrweb@2.1.1)': + '@amplitude/plugin-session-replay-browser@1.35.0(@amplitude/rrweb@2.1.1)': dependencies: - '@amplitude/analytics-client-common': 2.4.60 - '@amplitude/analytics-core': 2.54.2 + '@amplitude/analytics-core': 2.55.0 '@amplitude/analytics-types': 2.11.1 '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.40(@amplitude/rrweb@2.1.1) '@amplitude/rrweb-record': 2.0.0-alpha.40 - '@amplitude/session-replay-browser': 1.48.2(@amplitude/rrweb@2.1.1) + '@amplitude/session-replay-browser': 1.50.0(@amplitude/rrweb@2.1.1) tslib: 2.8.1 transitivePeerDependencies: - '@amplitude/rrweb' - '@amplitude/plugin-web-vitals-browser@1.1.45': + '@amplitude/plugin-web-vitals-browser@1.1.46': dependencies: - '@amplitude/analytics-core': 2.54.2 + '@amplitude/analytics-core': 2.55.0 tslib: 2.8.1 web-vitals: 5.1.0 @@ -9195,9 +9240,9 @@ snapshots: base64-arraybuffer: 1.0.2 mitt: 3.0.1 - '@amplitude/session-replay-browser@1.48.2(@amplitude/rrweb@2.1.1)': + '@amplitude/session-replay-browser@1.50.0(@amplitude/rrweb@2.1.1)': dependencies: - '@amplitude/analytics-core': 2.54.2 + '@amplitude/analytics-core': 2.55.0 '@amplitude/analytics-types': 2.11.1 '@amplitude/rrweb-plugin-console-record': 2.0.0-alpha.40(@amplitude/rrweb@2.1.1) '@amplitude/rrweb-record': 2.0.0-alpha.40 @@ -9360,12 +9405,12 @@ snapshots: '@chevrotain/types@11.1.2': {} - '@chromatic-com/storybook@5.3.0(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))': + '@chromatic-com/storybook@5.3.0(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 18.2.0 jsonfile: 6.2.1 - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) strip-ansi: 7.2.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -9567,6 +9612,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 @@ -9587,7 +9637,7 @@ snapshots: '@es-joy/jsdoccomment@0.88.0': dependencies: '@types/estree': 1.0.9 - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/types': 8.69.0 comment-parser: 1.4.7 esquery: 1.7.0 jsdoc-type-pratt-parser: 7.2.0 @@ -9595,7 +9645,7 @@ snapshots: '@es-joy/jsdoccomment@0.91.0': dependencies: '@types/estree': 1.0.9 - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/types': 8.69.0 comment-parser: 1.4.7 esquery: 1.7.0 jsdoc-type-pratt-parser: 8.0.0 @@ -9680,100 +9730,100 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) - ignore: 7.0.5 + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) + ignore: 7.0.8 - '@eslint-community/eslint-utils@4.9.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))': dependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint-react/ast@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@eslint-react/ast@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) string-ts: 2.3.1 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@eslint-react/core@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@eslint-react/core@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) ts-pattern: 5.9.0 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@eslint-react/eslint-plugin@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@eslint-react/eslint-plugin@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) - eslint-plugin-react-dom: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint-plugin-react-jsx: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint-plugin-react-naming-convention: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint-plugin-react-rsc: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint-plugin-react-web-api: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint-plugin-react-x: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) + eslint-plugin-react-dom: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint-plugin-react-jsx: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint-plugin-react-naming-convention: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint-plugin-react-rsc: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint-plugin-react-web-api: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint-plugin-react-x: 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@eslint-react/eslint@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@eslint-react/eslint@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@eslint-react/jsx@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@eslint-react/jsx@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) ts-pattern: 5.9.0 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@eslint-react/shared@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@eslint-react/shared@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) ts-pattern: 5.9.0 typescript: '@typescript/typescript6@6.0.2' - zod: 4.4.3 + zod: 4.5.4 transitivePeerDependencies: - supports-color - '@eslint-react/var@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@eslint-react/var@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) ts-pattern: 5.9.0 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: @@ -9931,9 +9981,9 @@ snapshots: '@hey-api/types@0.1.4': {} - '@hono/node-server@2.1.1(hono@4.13.3)': + '@hono/node-server@2.1.1(hono@4.13.5)': dependencies: - hono: 4.13.3 + hono: 4.13.5 '@humanfs/core@0.19.2': dependencies: @@ -9997,119 +10047,119 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.35.3': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.35.3': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.3': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.2': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.2': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.2': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.2': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.2': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.2': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.2': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.2': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.2': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.35.3': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.35.3': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.35.3': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.35.3': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.35.3': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.35.3': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.3': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.3': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.35.3': + '@img/sharp-wasm32@0.35.4': dependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.3': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.35.3': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.35.3': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.35.3': + '@img/sharp-win32-x64@0.35.4': optional: true '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(@typescript/typescript6@6.0.2) - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' optionalDependencies: typescript: '@typescript/typescript6@6.0.2' @@ -10370,9 +10420,9 @@ snapshots: '@types/react': 19.2.18 react: 19.2.8 - '@mediabunny/mp3-encoder@1.55.2(mediabunny@1.55.2)': + '@mediabunny/mp3-encoder@1.55.5(mediabunny@1.55.5)': dependencies: - mediabunny: 1.55.2 + mediabunny: 1.55.5 '@mermaid-js/parser@1.2.1': dependencies: @@ -10464,30 +10514,30 @@ snapshots: '@next/env@16.0.0': {} - '@next/env@16.3.3': {} + '@next/env@16.3.4': {} - '@next/swc-darwin-arm64@16.3.3': + '@next/swc-darwin-arm64@16.3.4': optional: true - '@next/swc-darwin-x64@16.3.3': + '@next/swc-darwin-x64@16.3.4': optional: true - '@next/swc-linux-arm64-gnu@16.3.3': + '@next/swc-linux-arm64-gnu@16.3.4': optional: true - '@next/swc-linux-arm64-musl@16.3.3': + '@next/swc-linux-arm64-musl@16.3.4': optional: true - '@next/swc-linux-x64-gnu@16.3.3': + '@next/swc-linux-x64-gnu@16.3.4': optional: true - '@next/swc-linux-x64-musl@16.3.3': + '@next/swc-linux-x64-musl@16.3.4': optional: true - '@next/swc-win32-arm64-msvc@16.3.3': + '@next/swc-win32-arm64-msvc@16.3.4': optional: true - '@next/swc-win32-x64-msvc@16.3.3': + '@next/swc-win32-x64-msvc@16.3.4': optional: true '@nodelib/fs.scandir@2.1.5': @@ -10560,11 +10610,11 @@ snapshots: transitivePeerDependencies: - '@opentelemetry/api' - '@orpc/tanstack-query@1.15.0(@orpc/client@1.15.0)(@tanstack/query-core@5.102.2)': + '@orpc/tanstack-query@1.15.0(@orpc/client@1.15.0)(@tanstack/query-core@5.102.8)': dependencies: '@orpc/client': 1.15.0 '@orpc/shared': 1.15.0 - '@tanstack/query-core': 5.102.2 + '@tanstack/query-core': 5.102.8 transitivePeerDependencies: - '@opentelemetry/api' @@ -10573,97 +10623,97 @@ snapshots: '@oxc-parser/binding-android-arm-eabi@0.127.0': optional: true - '@oxc-parser/binding-android-arm-eabi@0.143.0': + '@oxc-parser/binding-android-arm-eabi@0.147.0': optional: true '@oxc-parser/binding-android-arm64@0.127.0': optional: true - '@oxc-parser/binding-android-arm64@0.143.0': + '@oxc-parser/binding-android-arm64@0.147.0': optional: true '@oxc-parser/binding-darwin-arm64@0.127.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.143.0': + '@oxc-parser/binding-darwin-arm64@0.147.0': optional: true '@oxc-parser/binding-darwin-x64@0.127.0': optional: true - '@oxc-parser/binding-darwin-x64@0.143.0': + '@oxc-parser/binding-darwin-x64@0.147.0': optional: true '@oxc-parser/binding-freebsd-x64@0.127.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.143.0': + '@oxc-parser/binding-freebsd-x64@0.147.0': optional: true '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.143.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.147.0': optional: true '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.143.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.147.0': optional: true '@oxc-parser/binding-linux-arm64-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.143.0': + '@oxc-parser/binding-linux-arm64-gnu@0.147.0': optional: true '@oxc-parser/binding-linux-arm64-musl@0.127.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.143.0': + '@oxc-parser/binding-linux-arm64-musl@0.147.0': optional: true '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.143.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.147.0': optional: true '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.143.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.147.0': optional: true '@oxc-parser/binding-linux-riscv64-musl@0.127.0': optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.143.0': + '@oxc-parser/binding-linux-riscv64-musl@0.147.0': optional: true '@oxc-parser/binding-linux-s390x-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.143.0': + '@oxc-parser/binding-linux-s390x-gnu@0.147.0': optional: true '@oxc-parser/binding-linux-x64-gnu@0.127.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.143.0': + '@oxc-parser/binding-linux-x64-gnu@0.147.0': optional: true '@oxc-parser/binding-linux-x64-musl@0.127.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.143.0': + '@oxc-parser/binding-linux-x64-musl@0.147.0': optional: true '@oxc-parser/binding-openharmony-arm64@0.127.0': optional: true - '@oxc-parser/binding-openharmony-arm64@0.143.0': + '@oxc-parser/binding-openharmony-arm64@0.147.0': optional: true '@oxc-parser/binding-wasm32-wasi@0.127.0': @@ -10676,29 +10726,29 @@ snapshots: '@oxc-parser/binding-win32-arm64-msvc@0.127.0': optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.143.0': + '@oxc-parser/binding-win32-arm64-msvc@0.147.0': optional: true '@oxc-parser/binding-win32-ia32-msvc@0.127.0': optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.143.0': + '@oxc-parser/binding-win32-ia32-msvc@0.147.0': optional: true '@oxc-parser/binding-win32-x64-msvc@0.127.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.143.0': + '@oxc-parser/binding-win32-x64-msvc@0.147.0': optional: true '@oxc-project/runtime@0.146.0': {} '@oxc-project/types@0.127.0': {} - '@oxc-project/types@0.143.0': {} - '@oxc-project/types@0.146.0': {} + '@oxc-project/types@0.147.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.21.2': optional: true @@ -11057,83 +11107,83 @@ snapshots: dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 - picomatch: 4.0.5 + picomatch: 4.0.7 - '@sentry/browser-utils@10.70.0': + '@sentry/browser-utils@10.73.0': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 + '@sentry/core': 10.73.0 - '@sentry/browser@10.70.0': + '@sentry/browser@10.73.0': dependencies: - '@sentry/browser-utils': 10.70.0 + '@sentry/browser-utils': 10.73.0 '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 - '@sentry/feedback': 10.70.0 - '@sentry/replay': 10.70.0 - '@sentry/replay-canvas': 10.70.0 + '@sentry/core': 10.73.0 + '@sentry/feedback': 10.73.0 + '@sentry/replay': 10.73.0 + '@sentry/replay-canvas': 10.73.0 '@sentry/conventions@0.16.0': {} - '@sentry/core@10.70.0': + '@sentry/core@10.73.0': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/feedback@10.70.0': + '@sentry/feedback@10.73.0': dependencies: - '@sentry/core': 10.70.0 + '@sentry/core': 10.73.0 - '@sentry/react@10.70.0(react@19.2.8)': + '@sentry/react@10.73.0(react@19.2.8)': dependencies: - '@sentry/browser': 10.70.0 + '@sentry/browser': 10.73.0 '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 + '@sentry/core': 10.73.0 react: 19.2.8 - '@sentry/replay-canvas@10.70.0': + '@sentry/replay-canvas@10.73.0': dependencies: - '@sentry/core': 10.70.0 - '@sentry/replay': 10.70.0 + '@sentry/core': 10.73.0 + '@sentry/replay': 10.73.0 - '@sentry/replay@10.70.0': + '@sentry/replay@10.73.0': dependencies: - '@sentry/browser-utils': 10.70.0 - '@sentry/core': 10.70.0 + '@sentry/browser-utils': 10.73.0 + '@sentry/core': 10.73.0 - '@shikijs/core@4.4.2': + '@shikijs/core@4.4.3': dependencies: - '@shikijs/primitive': 4.4.2 - '@shikijs/types': 4.4.2 + '@shikijs/primitive': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.4.2': + '@shikijs/engine-javascript@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.4.2': + '@shikijs/engine-oniguruma@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.4.2': + '@shikijs/langs@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 - '@shikijs/primitive@4.4.2': + '@shikijs/primitive@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/themes@4.4.2': + '@shikijs/themes@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 - '@shikijs/types@4.4.2': + '@shikijs/types@4.4.3': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -11151,21 +11201,21 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-a11y@10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))': + '@storybook/addon-a11y@10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.13.0 - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) - '@storybook/addon-docs@10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))': + '@storybook/addon-docs@10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.18)(react@19.2.8) - '@storybook/csf-plugin': 10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + '@storybook/csf-plugin': 10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) '@storybook/icons': 2.1.0(react@19.2.8) - '@storybook/react-dom-shim': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + '@storybook/react-dom-shim': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) ts-dedent: 2.3.0 optionalDependencies: '@types/react': 19.2.18 @@ -11176,54 +11226,54 @@ snapshots: - vite - webpack - '@storybook/addon-links@10.5.10(@types/react@19.2.18)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))': + '@storybook/addon-links@10.5.10(@types/react@19.2.18)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))': dependencies: '@storybook/global': 5.0.0 - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) optionalDependencies: '@types/react': 19.2.18 react: 19.2.8 - '@storybook/addon-onboarding@10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))': + '@storybook/addon-onboarding@10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))': dependencies: - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) - '@storybook/addon-themes@10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))': + '@storybook/addon-themes@10.5.10(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))': dependencies: - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) ts-dedent: 2.3.0 - '@storybook/addon-vitest@10.5.10(@vitest/browser-playwright@4.1.11)(@vitest/browser@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11))(@vitest/runner@4.1.11)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(vitest@4.1.11)': + '@storybook/addon-vitest@10.5.10(@vitest/browser-playwright@4.1.11)(@vitest/browser@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11))(@vitest/runner@4.1.11)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(vitest@4.1.11)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.1.0(react@19.2.8) - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) - '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) + '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) '@vitest/runner': 4.1.11 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) transitivePeerDependencies: - react - '@storybook/builder-vite@10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))': + '@storybook/builder-vite@10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))': dependencies: - '@storybook/csf-plugin': 10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@storybook/csf-plugin': 10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) ts-dedent: 2.3.0 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))': + '@storybook/csf-plugin@10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))': dependencies: - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) unplugin: 2.3.11 optionalDependencies: esbuild: 0.28.2 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' '@storybook/global@5.0.0': {} @@ -11231,18 +11281,18 @@ snapshots: dependencies: react: 19.2.8 - '@storybook/nextjs-vite@10.5.10(@babel/core@7.29.7(supports-color@11.0.0))(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0)': + '@storybook/nextjs-vite@10.5.10(@babel/core@7.29.7(supports-color@11.0.0))(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0)': dependencies: - '@storybook/builder-vite': 10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) - '@storybook/react': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0) - '@storybook/react-vite': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0) - next: 16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@storybook/builder-vite': 10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) + '@storybook/react': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0) + '@storybook/react-vite': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0) + next: 16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@11.0.0))(react@19.2.8) - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' - vite-plugin-storybook-nextjs: 3.3.0(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0) + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' + vite-plugin-storybook-nextjs: 3.3.0(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.5(@types/react@19.2.18) @@ -11255,30 +11305,30 @@ snapshots: - supports-color - webpack - '@storybook/react-dom-shim@10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))': + '@storybook/react-dom-shim@10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))': dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@storybook/react-vite@10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0)': + '@storybook/react-vite@10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0)': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@rollup/pluginutils': 5.4.0 - '@storybook/builder-vite': 10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) - '@storybook/react': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0) + '@storybook/builder-vite': 10.5.10(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) + '@storybook/react': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0) empathic: 2.0.1 magic-string: 0.30.21 react: 19.2.8 react-docgen: 8.0.3(supports-color@11.0.0) react-dom: 19.2.8(react@19.2.8) resolve: 1.22.12 - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) tsconfig-paths: 4.2.0 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' optionalDependencies: typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: @@ -11289,15 +11339,15 @@ snapshots: - supports-color - webpack - '@storybook/react@10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0)': + '@storybook/react@10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))) + '@storybook/react-dom-shim': 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))) react: 19.2.8 react-docgen: 8.0.3(supports-color@11.0.0) react-docgen-typescript: 2.4.0(@typescript/typescript6@6.0.2) react-dom: 19.2.8(react@19.2.8) - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.5(@types/react@19.2.18) @@ -11320,19 +11370,19 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.4.3)': + '@t3-oss/env-core@0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.5.4)': optionalDependencies: typescript: '@typescript/typescript6@6.0.2' valibot: 1.4.2(@typescript/typescript6@6.0.2) - zod: 4.4.3 + zod: 4.5.4 - '@t3-oss/env-nextjs@0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.4.3)': + '@t3-oss/env-nextjs@0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.5.4)': dependencies: - '@t3-oss/env-core': 0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.4.3) + '@t3-oss/env-core': 0.13.11(@typescript/typescript6@6.0.2)(valibot@1.4.2(@typescript/typescript6@6.0.2))(zod@4.5.4) optionalDependencies: typescript: '@typescript/typescript6@6.0.2' valibot: 1.4.2(@typescript/typescript6@6.0.2) - zod: 4.4.3 + zod: 4.5.4 '@tailwindcss/node@4.3.3': dependencies: @@ -11403,19 +11453,19 @@ snapshots: postcss: 8.5.26 tailwindcss: 4.3.3 - '@tailwindcss/vite@4.3.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' '@tanstack/devtools-event-client@0.4.4': {} - '@tanstack/eslint-plugin-query@5.102.2(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@tanstack/eslint-plugin-query@5.102.8(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) optionalDependencies: typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: @@ -11433,7 +11483,7 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.102.2': {} + '@tanstack/query-core@5.102.8': {} '@tanstack/react-form@1.33.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: @@ -11450,9 +11500,9 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - '@tanstack/react-query@5.102.2(react@19.2.8)': + '@tanstack/react-query@5.102.8(react@19.2.8)': dependencies: - '@tanstack/query-core': 5.102.2 + '@tanstack/query-core': 5.102.8 react: 19.2.8 '@tanstack/react-store@0.11.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': @@ -11494,7 +11544,7 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 '@testing-library/dom': 10.4.1 @@ -11748,6 +11798,10 @@ snapshots: dependencies: undici-types: 8.3.0 + '@types/node@26.4.0': + dependencies: + undici-types: 8.3.0 + '@types/normalize-package-data@2.4.4': {} '@types/papaparse@5.5.2': @@ -11778,7 +11832,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 26.0.1 + '@types/node': 26.4.0 '@types/yauzl@2.10.3': dependencies: @@ -11787,56 +11841,56 @@ snapshots: '@types/zen-observable@0.8.3': {} - '@typescript-eslint/parser@8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@typescript-eslint/parser@8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) + '@typescript-eslint/visitor-keys': 8.69.0 debug: 4.4.3(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0)': + '@typescript-eslint/project-service@8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.67.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/tsconfig-utils': 8.69.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.69.0 debug: 4.4.3(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.67.0': + '@typescript-eslint/scope-manager@8.69.0': dependencies: - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 - '@typescript-eslint/tsconfig-utils@8.67.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.69.0(@typescript/typescript6@6.0.2)': dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@typescript-eslint/type-utils@8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) debug: 4.4.3(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.67.0': {} + '@typescript-eslint/types@8.69.0': {} - '@typescript-eslint/typescript-estree@8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0)': + '@typescript-eslint/typescript-estree@8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0)': dependencies: - '@typescript-eslint/project-service': 8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) - '@typescript-eslint/tsconfig-utils': 8.67.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/project-service': 8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) + '@typescript-eslint/tsconfig-utils': 8.69.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/visitor-keys': 8.69.0 debug: 4.4.3(supports-color@11.0.0) minimatch: 10.2.5 semver: 7.8.5 @@ -11846,20 +11900,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': + '@typescript-eslint/utils@8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.67.0': + '@typescript-eslint/visitor-keys@8.69.0': dependencies: - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/types': 8.69.0 eslint-visitor-keys: 5.0.1 '@typescript/typescript-aix-ppc64@7.0.2': @@ -11932,13 +11986,13 @@ snapshots: dependencies: unpic: 4.2.2 - '@unpic/react@1.0.2(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@unpic/react@1.0.2(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@unpic/core': 1.0.3 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - next: 16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@upsetjs/venn.js@2.0.0': optionalDependencies: @@ -11956,24 +12010,24 @@ snapshots: '@vinext/types@1.0.0-beta.2': {} - '@vitejs/devtools-kit@0.4.1(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(srvx@0.12.5)': + '@vitejs/devtools-kit@0.4.1(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(srvx@0.12.5)': dependencies: '@devframes/hub': 0.6.2(devframe@0.6.2(@typescript/typescript6@6.0.2)(srvx@0.12.5)) devframe: 0.6.2(@typescript/typescript6@6.0.2)(srvx@0.12.5) mlly: 1.8.2 nostics: 1.2.0 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' transitivePeerDependencies: - '@modelcontextprotocol/sdk' - srvx - typescript - '@vitejs/plugin-react@6.1.0(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + '@vitejs/plugin-react@6.1.1(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' - '@vitejs/plugin-rsc@0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': + '@vitejs/plugin-rsc@0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': dependencies: '@rolldown/pluginutils': 1.0.1 es-module-lexer: 2.3.1 @@ -11984,31 +12038,31 @@ snapshots: srvx: 0.12.5 strip-literal: 3.1.0 turbo-stream: 3.2.0 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' - vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' + vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) optionalDependencies: react-server-dom-webpack: 19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@vitest/browser-playwright@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11)': + '@vitest/browser-playwright@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11)': dependencies: - '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) - '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) playwright: 1.62.1 tinyrainbow: 3.1.0 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser-playwright@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11)': + '@vitest/browser-playwright@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11)': dependencies: - '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) - '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)) + '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)) playwright: 1.62.1 tinyrainbow: 3.1.0 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.12.0) transitivePeerDependencies: - bufferutil - msw @@ -12016,40 +12070,40 @@ snapshots: - vite optional: true - '@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11)': + '@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.6(@testing-library/dom@10.4.1) - '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11)': + '@vitest/browser-preview@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.6(@testing-library/dom@10.4.1) - '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.11.6) + '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.12.0) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11)': + '@vitest/browser@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/utils': 4.1.11 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) ws: 8.21.3 transitivePeerDependencies: - bufferutil @@ -12057,16 +12111,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11)': + '@vitest/browser@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)) '@vitest/utils': 4.1.11 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.12.0) ws: 8.21.3 transitivePeerDependencies: - bufferutil @@ -12086,9 +12140,9 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) optionalDependencies: - '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11) '@vitest/expect@3.2.4': dependencies: @@ -12107,21 +12161,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' - '@vitest/mocker@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)' '@vitest/pretty-format@3.2.4': dependencies: @@ -12161,7 +12215,7 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)': + '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)': dependencies: '@oxc-project/runtime': 0.146.0 '@oxc-project/types': 0.146.0 @@ -12182,11 +12236,11 @@ snapshots: esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 - tsx: 4.23.12 + tsx: 4.23.13 typescript: '@typescript/typescript6@6.0.2' yaml: 2.9.0 - '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)': + '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)': dependencies: '@oxc-project/runtime': 0.146.0 '@oxc-project/types': 0.146.0 @@ -12207,7 +12261,7 @@ snapshots: esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 - tsx: 4.23.12 + tsx: 4.23.13 typescript: 7.0.2 yaml: 2.9.0 @@ -12438,6 +12492,8 @@ snapshots: baseline-browser-mapping@2.10.40: {} + baseline-browser-mapping@2.11.20: {} + birecord@0.1.2: {} birpc@4.0.0: {} @@ -12467,13 +12523,21 @@ snapshots: node-releases: 2.0.50 update-browserslist-db: 1.2.3(browserslist@4.28.4) + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.20 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.417 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.8) + buffer-crc32@0.2.13: {} buffer-from@1.1.2: {} buffer-image-size@0.6.4: dependencies: - '@types/node': 26.0.1 + '@types/node': 26.4.0 buffer@5.7.1: dependencies: @@ -12527,6 +12591,8 @@ snapshots: caniuse-lite@1.0.30001799: {} + caniuse-lite@1.0.30001810: {} + canvas@3.2.3: dependencies: node-addon-api: 7.1.1 @@ -12667,6 +12733,8 @@ snapshots: comment-parser@1.4.7: {} + comment-parser@1.4.8: {} + compare-versions@6.1.1: {} concurrently@10.0.5: @@ -12690,9 +12758,9 @@ snapshots: copy-to-clipboard@4.0.2: {} - core-js-compat@3.49.0: + core-js-compat@3.50.0: dependencies: - browserslist: 4.28.4 + browserslist: 4.28.8 cose-base@1.0.3: dependencies: @@ -12977,6 +13045,11 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.1 + default-browser@5.5.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + define-lazy-prop@3.0.0: {} defu@6.1.7: {} @@ -13066,6 +13139,8 @@ snapshots: electron-to-chromium@1.5.380: {} + electron-to-chromium@1.5.417: {} + elkjs@0.11.1: {} embla-carousel-autoplay@8.6.0(embla-carousel@8.6.0): @@ -13146,6 +13221,8 @@ snapshots: es-toolkit@1.51.0: {} + es-toolkit@1.52.0: {} + esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -13183,32 +13260,32 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-compat-utils@0.5.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-compat-utils@0.5.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) semver: 7.8.5 - eslint-json-compat-utils@0.2.3(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(jsonc-eslint-parser@3.3.0): + eslint-json-compat-utils@0.2.3(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(jsonc-eslint-parser@3.3.0): dependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) esquery: 1.7.0 jsonc-eslint-parser: 3.3.0 - eslint-markdown@0.12.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-markdown@0.12.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: '@eslint/markdown': 7.5.1(supports-color@11.0.0) micromark-util-normalize-identifier: 2.0.1 parse5: 8.0.1 optionalDependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) transitivePeerDependencies: - supports-color - eslint-plugin-antfu@3.2.3(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-antfu@3.2.3(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) - eslint-plugin-better-tailwindcss@4.7.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(tailwindcss@4.3.3): + eslint-plugin-better-tailwindcss@4.7.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(tailwindcss@4.3.3): dependencies: '@eslint/css-tree': 4.0.5 '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(@typescript/typescript6@6.0.2)) @@ -13220,37 +13297,37 @@ snapshots: tsconfig-paths-webpack-plugin: 4.2.0 valibot: 1.4.2(@typescript/typescript6@6.0.2) optionalDependencies: - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) - oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) + oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) transitivePeerDependencies: - '@eslint/css' - typescript - eslint-plugin-command@3.5.3(@typescript-eslint/typescript-estree@8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0))(@typescript-eslint/utils@8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-command@3.5.3(@typescript-eslint/typescript-estree@8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0))(@typescript-eslint/utils@8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: '@es-joy/jsdoccomment': 0.88.0 - '@typescript-eslint/typescript-estree': 8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@typescript-eslint/typescript-estree': 8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) - eslint-plugin-erasable-syntax-only@0.4.2(@typescript-eslint/parser@8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-erasable-syntax-only@0.4.2(@typescript-eslint/parser@8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0))(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@typescript-eslint/parser': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/parser': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) cached-factory: 0.3.0 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - eslint-plugin-es-x@7.8.0(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-es-x@7.8.0(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) '@eslint-community/regexpp': 4.12.2 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) - eslint-compat-utils: 0.5.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) + eslint-compat-utils: 0.5.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) - eslint-plugin-jsdoc@63.3.3(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-jsdoc@63.3.3(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: '@es-joy/jsdoccomment': 0.91.0 '@es-joy/resolve.exports': 1.2.0 @@ -13258,7 +13335,7 @@ snapshots: comment-parser: 1.4.7 debug: 4.4.3(supports-color@11.0.0) escape-string-regexp: 4.0.0 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) espree: 11.2.0 esquery: 1.7.0 html-entities: 2.6.0 @@ -13270,27 +13347,27 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-jsonc@3.4.2(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-jsonc@3.4.2(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 '@ota-meshi/ast-token-store': 0.3.0 diff-sequences: 29.6.3 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) - eslint-json-compat-utils: 0.2.3(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(jsonc-eslint-parser@3.3.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) + eslint-json-compat-utils: 0.2.3(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(jsonc-eslint-parser@3.3.0) jsonc-eslint-parser: 3.3.0 natural-compare: 1.4.0 synckit: 0.11.13 transitivePeerDependencies: - '@eslint/json' - eslint-plugin-markdown-preferences@0.41.1(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-markdown-preferences@0.41.1(@eslint/markdown@8.0.3(supports-color@11.0.0))(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: '@eslint/markdown': 8.0.3(supports-color@11.0.0) diff-sequences: 29.6.3 emoji-regex-xs: 2.0.1 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) mdast-util-from-markdown: 2.0.3(supports-color@11.0.0) mdast-util-frontmatter: 2.0.1(supports-color@11.0.0) mdast-util-gfm: 3.1.0(supports-color@11.0.0) @@ -13305,13 +13382,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-n@18.3.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-n@18.3.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) enhanced-resolve: 5.24.1 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) - eslint-plugin-es-x: 7.8.0(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) - get-tsconfig: 4.14.1 + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) + eslint-plugin-es-x: 7.8.0(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) + get-tsconfig: 4.14.3 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 @@ -13319,28 +13396,28 @@ snapshots: optionalDependencies: typescript: '@typescript/typescript6@6.0.2' - eslint-plugin-no-barrel-files@1.3.1(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-no-barrel-files@1.3.1(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-perfectionist@5.10.1(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-perfectionist@5.11.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-pnpm@1.8.0(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-pnpm@1.8.0(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: empathic: 2.0.1 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) - eslint-json-compat-utils: 0.2.3(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(jsonc-eslint-parser@3.3.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) + eslint-json-compat-utils: 0.2.3(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(jsonc-eslint-parser@3.3.0) jsonc-eslint-parser: 3.3.0 pathe: 2.0.3 pnpm-workspace-yaml: 1.8.0 @@ -13350,94 +13427,94 @@ snapshots: transitivePeerDependencies: - '@eslint/json' - eslint-plugin-react-dom@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-react-dom@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/jsx': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/jsx': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) compare-versions: 6.1.1 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - eslint-plugin-react-jsx@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-react-jsx@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/jsx': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/jsx': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-react-naming-convention@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) ts-pattern: 5.9.0 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - eslint-plugin-react-rsc@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-react-rsc@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - eslint-plugin-react-web-api@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-react-web-api@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) birecord: 0.1.2 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) ts-pattern: 5.9.0 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - eslint-plugin-react-x@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-react-x@5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/jsx': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/type-utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/ast': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/core': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/eslint': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/jsx': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/shared': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@eslint-react/var': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/scope-manager': 8.69.0 + '@typescript-eslint/type-utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/typescript-estree': 8.69.0(@typescript/typescript6@6.0.2)(supports-color@11.0.0) + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) compare-versions: 6.1.1 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) string-ts: 2.3.1 ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) ts-pattern: 5.9.0 @@ -13445,48 +13522,48 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-regexp@3.1.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-regexp@3.1.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) '@eslint-community/regexpp': 4.12.2 - comment-parser: 1.4.7 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) - jsdoc-type-pratt-parser: 7.2.0 + comment-parser: 1.4.8 + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) + jsdoc-type-pratt-parser: 7.3.0 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-storybook@10.5.10(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-storybook@10.5.10(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/utils': 8.67.0(@typescript/typescript6@6.0.2)(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + '@typescript-eslint/types': 8.69.0 + '@typescript-eslint/utils': 8.69.0(@typescript/typescript6@6.0.2)(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-toml@1.5.0(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): + eslint-plugin-toml@1.5.0(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0): dependencies: '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 '@ota-meshi/ast-token-store': 0.3.0 debug: 4.4.3(supports-color@11.0.0) - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) toml-eslint-parser: 1.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-unicorn@71.1.0(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-unicorn@71.1.0(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) browserslist: 4.28.4 change-case: 5.4.4 ci-info: 4.4.0 - core-js-compat: 3.49.0 + core-js-compat: 3.50.0 detect-indent: 7.0.2 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) find-up-simple: 1.0.1 - globals: 17.7.0 + globals: 17.11.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 is-identifier: 1.1.0 @@ -13497,14 +13574,14 @@ snapshots: semver: 7.8.5 strip-indent: 4.1.1 - eslint-plugin-yml@3.8.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)): + eslint-plugin-yml@3.8.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)): dependencies: '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 '@ota-meshi/ast-token-store': 0.3.0 diff-sequences: 29.6.3 escape-string-regexp: 5.0.0 - eslint: 10.9.0(jiti@2.7.0)(supports-color@11.0.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@11.0.0) natural-compare: 1.4.0 yaml-eslint-parser: 2.1.0 @@ -13519,9 +13596,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0): + eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.0(jiti@2.7.0)(supports-color@11.0.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@11.0.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5(supports-color@11.0.0) '@eslint/config-helpers': 0.7.0 @@ -13637,9 +13714,9 @@ snapshots: dependencies: walk-up-path: 4.0.0 - fdir@6.5.0(picomatch@4.0.5): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 fflate@0.7.4: {} @@ -13664,18 +13741,19 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.4 keyv: 4.5.4 - flatted@3.4.2: {} + flatted@3.4.4: {} format@0.2.2: {} - formatly@0.3.0: + formatly@0.7.0: dependencies: fd-package-json: 2.0.0 + package-manager-detector: 1.8.0 - foxact@0.3.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + foxact@0.3.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: client-only: 0.0.1 event-target-bus: 1.1.0 @@ -13720,7 +13798,7 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - get-tsconfig@4.14.1: + get-tsconfig@4.14.3: dependencies: resolve-pkg-maps: 1.0.0 @@ -13751,7 +13829,7 @@ snapshots: globals@15.15.0: {} - globals@17.7.0: {} + globals@17.11.0: {} globrex@0.1.2: {} @@ -13766,9 +13844,9 @@ snapshots: hachure-fill@0.5.2: {} - happy-dom@20.11.6: + happy-dom@20.12.0: dependencies: - '@types/node': 26.0.1 + '@types/node': 26.4.0 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 buffer-image-size: 0.6.4 @@ -13913,7 +13991,7 @@ snapshots: hex-rgb@4.3.0: {} - hono@4.13.3: {} + hono@4.13.5: {} hosted-git-info@9.0.3: dependencies: @@ -13969,7 +14047,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.8: {} image-size@2.0.2: {} @@ -14073,20 +14151,20 @@ snapshots: jiti@2.7.0: {} - jotai-scope@0.11.0(jotai@2.20.2(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8): + jotai-scope@0.11.0(jotai@2.20.3(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8): dependencies: - jotai: 2.20.2(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8) + jotai: 2.20.3(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8) react: 19.2.8 - jotai-tanstack-query@0.11.0(@tanstack/query-core@5.102.2)(@tanstack/react-query@5.102.2(react@19.2.8))(jotai@2.20.2(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8): + jotai-tanstack-query@0.11.0(@tanstack/query-core@5.102.8)(@tanstack/react-query@5.102.8(react@19.2.8))(jotai@2.20.3(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8): dependencies: - '@tanstack/query-core': 5.102.2 - jotai: 2.20.2(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8) + '@tanstack/query-core': 5.102.8 + jotai: 2.20.3(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@tanstack/react-query': 5.102.2(react@19.2.8) + '@tanstack/react-query': 5.102.8(react@19.2.8) react: 19.2.8 - jotai@2.20.2(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8): + jotai@2.20.3(@babel/core@7.29.7(supports-color@11.0.0))(@babel/template@7.29.7)(@types/react@19.2.18)(react@19.2.8): optionalDependencies: '@babel/core': 7.29.7(supports-color@11.0.0) '@babel/template': 7.29.7 @@ -14107,12 +14185,14 @@ snapshots: dependencies: argparse: 2.0.1 - js-yaml@5.3.0: + js-yaml@5.4.1: dependencies: argparse: 2.0.1 jsdoc-type-pratt-parser@7.2.0: {} + jsdoc-type-pratt-parser@7.3.0: {} + jsdoc-type-pratt-parser@8.0.0: {} jsesc@3.1.0: {} @@ -14155,16 +14235,16 @@ snapshots: khroma@2.1.0: {} - knip@6.32.2: + knip@6.34.0: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - formatly: 0.3.0 - get-tsconfig: 4.14.1 + fdir: 6.5.0(picomatch@4.0.7) + formatly: 0.7.0 + get-tsconfig: 4.14.3 jiti: 2.7.0 - oxc-parser: 0.143.0 + oxc-parser: 0.147.0 oxc-resolver: 11.24.2 - picomatch: 4.0.5 - smol-toml: 1.7.1 + picomatch: 4.0.7 + smol-toml: 1.8.0 strip-json-comments: 5.0.3 tinyglobby: 0.2.17 unbash: 4.0.10 @@ -14177,7 +14257,7 @@ snapshots: kolorist@1.8.0: {} - ky@2.0.2: {} + ky@2.1.0: {} launch-ide@1.4.5: dependencies: @@ -14342,7 +14422,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loro-crdt@1.14.1: {} + loro-crdt@1.15.1: {} loupe@3.2.1: {} @@ -14583,14 +14663,14 @@ snapshots: mdn-data@2.29.0: {} - mediabunny@1.55.2: + mediabunny@1.55.5: dependencies: '@types/dom-mediacapture-transform': 0.1.11 '@types/dom-webcodecs': 0.1.13 merge2@1.4.1: {} - mermaid@11.17.0: + mermaid@11.17.2: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.3 @@ -14915,9 +14995,9 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.3.3 + '@next/env': 16.3.4 '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.10.40 caniuse-lite: 1.0.30001799 @@ -14926,16 +15006,16 @@ snapshots: react-dom: 19.2.8(react@19.2.8) styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@11.0.0))(react@19.2.8) optionalDependencies: - '@next/swc-darwin-arm64': 16.3.3 - '@next/swc-darwin-x64': 16.3.3 - '@next/swc-linux-arm64-gnu': 16.3.3 - '@next/swc-linux-arm64-musl': 16.3.3 - '@next/swc-linux-x64-gnu': 16.3.3 - '@next/swc-linux-x64-musl': 16.3.3 - '@next/swc-win32-arm64-msvc': 16.3.3 - '@next/swc-win32-x64-msvc': 16.3.3 + '@next/swc-darwin-arm64': 16.3.4 + '@next/swc-darwin-x64': 16.3.4 + '@next/swc-linux-arm64-gnu': 16.3.4 + '@next/swc-linux-arm64-musl': 16.3.4 + '@next/swc-linux-x64-gnu': 16.3.4 + '@next/swc-linux-x64-musl': 16.3.4 + '@next/swc-win32-arm64-msvc': 16.3.4 + '@next/swc-win32-x64-msvc': 16.3.4 '@playwright/test': 1.62.1 - sharp: 0.35.3(@types/node@25.9.5) + sharp: 0.35.4(@types/node@25.9.5) transitivePeerDependencies: - '@babel/core' - '@types/node' @@ -14951,7 +15031,9 @@ snapshots: node-releases@2.0.50: {} - node@runtime:22.23.2: {} + node-releases@2.0.54: {} + + node@runtime:24.20.0: {} normalize-package-data@8.0.0: dependencies: @@ -14967,12 +15049,12 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.10.0(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): + nuqs@2.10.1(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): dependencies: '@standard-schema/spec': 1.1.0 react: 19.2.8 optionalDependencies: - next: 16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) object-assign@4.1.1: {} @@ -15023,6 +15105,15 @@ snapshots: powershell-utils: 0.2.0 wsl-utils: 1.0.0 + open@11.0.2: + dependencies: + default-browser: 5.5.1 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.2.1 + wsl-utils: 1.0.0 + openapi-types@12.1.3: {} optionator@0.9.4: @@ -15070,29 +15161,29 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 '@oxc-parser/binding-win32-x64-msvc': 0.127.0 - oxc-parser@0.143.0: + oxc-parser@0.147.0: dependencies: - '@oxc-project/types': 0.143.0 + '@oxc-project/types': 0.147.0 optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.143.0 - '@oxc-parser/binding-android-arm64': 0.143.0 - '@oxc-parser/binding-darwin-arm64': 0.143.0 - '@oxc-parser/binding-darwin-x64': 0.143.0 - '@oxc-parser/binding-freebsd-x64': 0.143.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.143.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.143.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.143.0 - '@oxc-parser/binding-linux-arm64-musl': 0.143.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.143.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.143.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.143.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.143.0 - '@oxc-parser/binding-linux-x64-gnu': 0.143.0 - '@oxc-parser/binding-linux-x64-musl': 0.143.0 - '@oxc-parser/binding-openharmony-arm64': 0.143.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.143.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.143.0 - '@oxc-parser/binding-win32-x64-msvc': 0.143.0 + '@oxc-parser/binding-android-arm-eabi': 0.147.0 + '@oxc-parser/binding-android-arm64': 0.147.0 + '@oxc-parser/binding-darwin-arm64': 0.147.0 + '@oxc-parser/binding-darwin-x64': 0.147.0 + '@oxc-parser/binding-freebsd-x64': 0.147.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.147.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.147.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.147.0 + '@oxc-parser/binding-linux-arm64-musl': 0.147.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.147.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.147.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.147.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.147.0 + '@oxc-parser/binding-linux-x64-gnu': 0.147.0 + '@oxc-parser/binding-linux-x64-musl': 0.147.0 + '@oxc-parser/binding-openharmony-arm64': 0.147.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.147.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.147.0 + '@oxc-parser/binding-win32-x64-msvc': 0.147.0 oxc-resolver@11.21.2: optionalDependencies: @@ -15138,7 +15229,7 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 - oxfmt@0.64.0(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): + oxfmt@0.64.0(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): dependencies: tinypool: 2.1.0 optionalDependencies: @@ -15161,9 +15252,9 @@ snapshots: '@oxfmt/binding-win32-arm64-msvc': 0.64.0 '@oxfmt/binding-win32-ia32-msvc': 0.64.0 '@oxfmt/binding-win32-x64-msvc': 0.64.0 - vite-plus: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite-plus: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) - oxfmt@0.64.0(vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)): + oxfmt@0.64.0(vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)): dependencies: tinypool: 2.1.0 optionalDependencies: @@ -15186,7 +15277,7 @@ snapshots: '@oxfmt/binding-win32-arm64-msvc': 0.64.0 '@oxfmt/binding-win32-ia32-msvc': 0.64.0 '@oxfmt/binding-win32-x64-msvc': 0.64.0 - vite-plus: 0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0) + vite-plus: 0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0) oxlint-tsgolint@7.0.2001: optionalDependencies: @@ -15197,7 +15288,7 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 7.0.2001 '@oxlint-tsgolint/win32-x64': 7.0.2001 - oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): + oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.79.0 '@oxlint/binding-android-arm64': 1.79.0 @@ -15219,9 +15310,9 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.79.0 '@oxlint/binding-win32-x64-msvc': 1.79.0 oxlint-tsgolint: 7.0.2001 - vite-plus: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite-plus: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) - oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)): + oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.79.0 '@oxlint/binding-android-arm64': 1.79.0 @@ -15243,7 +15334,7 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.79.0 '@oxlint/binding-win32-x64-msvc': 1.79.0 oxlint-tsgolint: 7.0.2001 - vite-plus: 0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0) + vite-plus: 0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0) p-event@6.0.1: dependencies: @@ -15261,6 +15352,8 @@ snapshots: package-manager-detector@1.6.0: {} + package-manager-detector@1.8.0: {} + pad-right@0.2.2: dependencies: repeat-string: 1.6.1 @@ -15350,6 +15443,8 @@ snapshots: picomatch@4.0.5: {} + picomatch@4.0.7: {} + pinyin-pro@3.29.3: {} pkg-types@1.3.1: @@ -15412,6 +15507,8 @@ snapshots: powershell-utils@0.2.0: {} + powershell-utils@0.2.1: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -15457,7 +15554,7 @@ snapshots: dependencies: react: 19.2.8 - qs@6.15.3: + qs@6.16.0: dependencies: es-define-property: 1.0.1 side-channel: '@nolyfill/side-channel@1.0.44' @@ -15753,7 +15850,7 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - remend@1.3.0: {} + remend@1.3.1: {} repeat-string@1.6.1: {} @@ -15843,37 +15940,37 @@ snapshots: server-only@0.0.1: {} - sharp@0.35.3(@types/node@25.9.5): + sharp@0.35.4(@types/node@25.9.5): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.3 - '@img/sharp-darwin-x64': 0.35.3 - '@img/sharp-freebsd-wasm32': 0.35.3 - '@img/sharp-libvips-darwin-arm64': 1.3.2 - '@img/sharp-libvips-darwin-x64': 1.3.2 - '@img/sharp-libvips-linux-arm': 1.3.2 - '@img/sharp-libvips-linux-arm64': 1.3.2 - '@img/sharp-libvips-linux-ppc64': 1.3.2 - '@img/sharp-libvips-linux-riscv64': 1.3.2 - '@img/sharp-libvips-linux-s390x': 1.3.2 - '@img/sharp-libvips-linux-x64': 1.3.2 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 - '@img/sharp-linux-arm': 0.35.3 - '@img/sharp-linux-arm64': 0.35.3 - '@img/sharp-linux-ppc64': 0.35.3 - '@img/sharp-linux-riscv64': 0.35.3 - '@img/sharp-linux-s390x': 0.35.3 - '@img/sharp-linux-x64': 0.35.3 - '@img/sharp-linuxmusl-arm64': 0.35.3 - '@img/sharp-linuxmusl-x64': 0.35.3 - '@img/sharp-webcontainers-wasm32': 0.35.3 - '@img/sharp-win32-arm64': 0.35.3 - '@img/sharp-win32-ia32': 0.35.3 - '@img/sharp-win32-x64': 0.35.3 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 '@types/node': 25.9.5 optional: true @@ -15885,14 +15982,14 @@ snapshots: shell-quote@1.9.0: {} - shiki@4.4.2: + shiki@4.4.3: dependencies: - '@shikijs/core': 4.4.2 - '@shikijs/engine-javascript': 4.4.2 - '@shikijs/engine-oniguruma': 4.4.2 - '@shikijs/langs': 4.4.2 - '@shikijs/themes': 4.4.2 - '@shikijs/types': 4.4.2 + '@shikijs/core': 4.4.3 + '@shikijs/engine-javascript': 4.4.3 + '@shikijs/engine-oniguruma': 4.4.3 + '@shikijs/langs': 4.4.3 + '@shikijs/themes': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -15920,7 +16017,7 @@ snapshots: size-sensor@1.0.3: {} - smol-toml@1.7.1: {} + smol-toml@1.8.0: {} socket.io-client@4.8.3(supports-color@11.0.0): dependencies: @@ -15988,7 +16085,7 @@ snapshots: stdin-discarder@0.3.2: {} - storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): + storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.1.0(react@19.2.8) @@ -16009,19 +16106,18 @@ snapshots: ws: 8.21.3 optionalDependencies: '@types/react': 19.2.18 - vite-plus: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) + vite-plus: 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) transitivePeerDependencies: - bufferutil - react - utf-8-validate - streamdown@2.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@11.0.0): + streamdown@2.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@11.0.0): dependencies: clsx: 2.1.1 hast-util-to-jsx-runtime: 2.3.6(supports-color@11.0.0) html-url-attributes: 3.0.1 marked: 17.0.6 - mermaid: 11.17.0 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) rehype-harden: 1.1.8 @@ -16030,7 +16126,7 @@ snapshots: remark-gfm: 4.0.1(supports-color@11.0.0) remark-parse: 11.0.0(supports-color@11.0.0) remark-rehype: 11.1.2 - remend: 1.3.0 + remend: 1.3.1 tailwind-merge: 3.6.0 unified: 11.0.5 unist-util-visit: 5.1.0 @@ -16184,8 +16280,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 tinypool@2.1.0: {} @@ -16195,11 +16291,11 @@ snapshots: tinyspy@4.0.4: {} - tldts-core@7.4.10: {} + tldts-core@7.4.11: {} - tldts@7.4.10: + tldts@7.4.11: dependencies: - tldts-core: 7.4.10 + tldts-core: 7.4.11 to-regex-range@5.0.1: dependencies: @@ -16257,7 +16353,7 @@ snapshots: tslib@2.8.1: {} - tsx@4.23.12: + tsx@4.23.13: dependencies: esbuild: 0.28.2 optionalDependencies: @@ -16382,7 +16478,7 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.17.0 - picomatch: 4.0.5 + picomatch: 4.0.7 webpack-virtual-modules: 0.6.2 update-browserslist-db@1.2.3(browserslist@4.28.4): @@ -16391,6 +16487,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.3.2(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -16456,21 +16558,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@1.0.0-beta.8(@vitejs/plugin-react@6.1.0(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): + vinext@1.0.0-beta.8(@vitejs/plugin-react@6.1.1(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(@vitejs/plugin-rsc@0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): dependencies: - '@unpic/react': 1.0.2(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@unpic/react': 1.0.2(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@vercel/og': 0.8.6 '@vinext/types': 1.0.0-beta.2 - '@vitejs/plugin-react': 6.1.0(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitejs/plugin-react': 6.1.1(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) ipaddr.js: 2.4.0 magic-string: 0.30.21 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vite-plugin-commonjs: 0.10.4 web-vitals: 4.2.4 optionalDependencies: - '@vitejs/plugin-rsc': 0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + '@vitejs/plugin-rsc': 0.5.34(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react-server-dom-webpack@19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) react-server-dom-webpack: 19.2.8(react-dom@19.2.8(react@19.2.8))(react@19.2.8) transitivePeerDependencies: - next @@ -16488,9 +16590,9 @@ snapshots: fast-glob: 3.3.3 magic-string: 0.30.21 - vite-plugin-inspect@12.0.2(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(srvx@0.12.5): + vite-plugin-inspect@12.0.2(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(srvx@0.12.5): dependencies: - '@vitejs/devtools-kit': 0.4.1(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(srvx@0.12.5) + '@vitejs/devtools-kit': 0.4.1(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(srvx@0.12.5) ansis: 4.3.1 error-stack-parser-es: 2.0.1 obug: 2.1.3 @@ -16499,47 +16601,47 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.2 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' transitivePeerDependencies: - '@modelcontextprotocol/sdk' - srvx - typescript - vite-plugin-storybook-nextjs@3.3.0(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(next@16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)))(supports-color@11.0.0): + vite-plugin-storybook-nextjs@3.3.0(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(next@16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(storybook@10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)))(supports-color@11.0.0): dependencies: '@next/env': 16.0.0 image-size: 2.0.2 magic-string: 0.30.21 module-alias: 2.3.4 - next: 16.3.3(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + next: 16.3.4(@babel/core@7.29.7(supports-color@11.0.0))(@playwright/test@1.62.1)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + storybook: 10.5.10(@types/react@19.2.18)(react@19.2.8)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) ts-dedent: 2.3.0 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' - vite-tsconfig-paths: 5.1.4(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(supports-color@11.0.0) + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' + vite-tsconfig-paths: 5.1.4(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(supports-color@11.0.0) transitivePeerDependencies: - supports-color - typescript - vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0): + vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0): dependencies: '@oxc-project/types': 0.146.0 '@oxlint/plugins': 1.79.0 - '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) - '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11) '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 '@vitest/spy': 4.1.11 '@vitest/utils': 4.1.11 - '@voidzero-dev/vite-plus-core': 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0) - oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) - oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@voidzero-dev/vite-plus-core': 0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) + oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) oxlint-tsgolint: 7.0.2001 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) optionalDependencies: - '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) + '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) '@voidzero-dev/vite-plus-darwin-arm64': 0.3.0 '@voidzero-dev/vite-plus-darwin-x64': 0.3.0 '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.3.0 @@ -16579,26 +16681,26 @@ snapshots: - vite - yaml - vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0): + vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0): dependencies: '@oxc-project/types': 0.146.0 '@oxlint/plugins': 1.79.0 - '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) - '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 '@vitest/spy': 4.1.11 '@vitest/utils': 4.1.11 - '@voidzero-dev/vite-plus-core': 0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0) - oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)) - oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.11.6)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)) + '@voidzero-dev/vite-plus-core': 0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0) + oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)) + oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(esbuild@0.28.2)(happy-dom@20.12.0)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)) oxlint-tsgolint: 7.0.2001 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.12.0) optionalDependencies: - '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) + '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) '@voidzero-dev/vite-plus-darwin-arm64': 0.3.0 '@voidzero-dev/vite-plus-darwin-x64': 0.3.0 '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.3.0 @@ -16638,26 +16740,26 @@ snapshots: - vite - yaml - vite-tsconfig-paths@5.1.4(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(supports-color@11.0.0): + vite-tsconfig-paths@5.1.4(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(supports-color@11.0.0): dependencies: debug: 4.4.3(supports-color@11.0.0) globrex: 0.1.2 tsconfck: 3.1.6(@typescript/typescript6@6.0.2) optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' transitivePeerDependencies: - supports-color - typescript - vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)): + vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' vitest-browser-react@2.2.0(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11): dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) optionalDependencies: '@types/react': 19.2.18 '@types/react-dom': 19.2.5(@types/react@19.2.18) @@ -16666,12 +16768,12 @@ snapshots: dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6) + vitest: 4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0) - vitest@4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(happy-dom@20.11.6): + vitest@4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(happy-dom@20.12.0): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -16682,27 +16784,27 @@ snapshots: magic-string: 0.30.21 obug: 2.1.3 pathe: 2.0.3 - picomatch: 4.0.5 + picomatch: 4.0.7 std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)' why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.5 - '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) - '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) + '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@4.1.11) '@vitest/coverage-v8': 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11) - happy-dom: 20.11.6 + happy-dom: 20.12.0 transitivePeerDependencies: - msw - vitest@4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.11.6): + vitest@4.1.11(@types/node@25.9.5)(@vitest/browser-playwright@4.1.11)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(happy-dom@20.12.0): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -16713,20 +16815,20 @@ snapshots: magic-string: 0.30.21 obug: 2.1.3 pathe: 2.0.3 - picomatch: 4.0.5 + picomatch: 4.0.7 std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0)' why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.5 - '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) - '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser-playwright': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(playwright@1.62.1)(vitest@4.1.11) + '@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@25.9.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.11) '@vitest/coverage-v8': 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11) - happy-dom: 20.11.6 + happy-dom: 20.12.0 transitivePeerDependencies: - msw @@ -16873,6 +16975,8 @@ snapshots: zod@4.4.3: {} + zod@4.5.4: {} + zrender@6.1.0: dependencies: tslib: 2.3.0 @@ -16899,8 +17003,8 @@ snapshots: zwitch@2.0.4: {} time: - '@amplitude/analytics-browser@2.45.6': '2026-08-12T17:50:48.412Z' - '@amplitude/plugin-session-replay-browser@1.33.8': '2026-08-12T17:46:12.315Z' + '@amplitude/analytics-browser@2.45.8': '2026-08-27T22:21:54.725Z' + '@amplitude/plugin-session-replay-browser@1.35.0': '2026-08-31T19:27:50.780Z' '@axe-core/playwright@4.13.0': '2026-08-11T17:07:40.763Z' '@base-ui/react@1.7.0': '2026-08-04T09:55:20.089Z' '@chromatic-com/storybook@5.3.0': '2026-08-06T08:33:46.580Z' @@ -16923,7 +17027,7 @@ time: '@lexical/selection@0.47.0': '2026-07-09T21:14:16.798Z' '@lexical/text@0.47.0': '2026-07-09T21:14:30.676Z' '@lexical/utils@0.47.0': '2026-07-09T21:14:36.426Z' - '@mediabunny/mp3-encoder@1.55.2': '2026-08-21T18:21:45.711Z' + '@mediabunny/mp3-encoder@1.55.5': '2026-08-31T12:17:39.218Z' '@monaco-editor/react@4.7.0': '2025-02-13T16:13:41.390Z' '@napi-rs/keyring@1.3.0': '2026-04-30T09:56:44.246Z' '@orpc/client@1.15.0': '2026-08-08T13:52:09.241Z' @@ -16933,7 +17037,7 @@ time: '@playwright/test@1.62.1': '2026-07-30T16:36:55.324Z' '@remixicon/react@4.9.0': '2026-01-29T10:53:18.993Z' '@rgrove/parse-xml@4.2.3': '2026-07-26T23:12:17.833Z' - '@sentry/react@10.70.0': '2026-08-10T11:05:59.874Z' + '@sentry/react@10.73.0': '2026-08-31T16:57:44.443Z' '@storybook/addon-a11y@10.5.10': '2026-08-20T10:45:18.990Z' '@storybook/addon-docs@10.5.10': '2026-08-20T10:48:20.529Z' '@storybook/addon-links@10.5.10': '2026-08-20T10:44:25.582Z' @@ -16949,16 +17053,16 @@ time: '@t3-oss/env-nextjs@0.13.11': '2026-03-22T19:16:09.026Z' '@tailwindcss/postcss@4.3.3': '2026-07-16T12:03:56.054Z' '@tailwindcss/vite@4.3.3': '2026-07-16T12:04:06.316Z' - '@tanstack/eslint-plugin-query@5.102.2': '2026-08-23T18:00:24.776Z' + '@tanstack/eslint-plugin-query@5.102.8': '2026-08-27T16:07:09.822Z' '@tanstack/form-core@1.33.5': '2026-08-11T12:45:38.255Z' - '@tanstack/query-core@5.102.2': '2026-08-23T18:00:26.529Z' + '@tanstack/query-core@5.102.8': '2026-08-27T16:06:49.682Z' '@tanstack/react-form@1.33.5': '2026-08-11T12:45:38.642Z' '@tanstack/react-hotkeys@0.10.0': '2026-04-25T12:28:06.989Z' - '@tanstack/react-query@5.102.2': '2026-08-23T18:00:30.589Z' + '@tanstack/react-query@5.102.8': '2026-08-27T16:06:57.089Z' '@tanstack/react-virtual@3.14.10': '2026-08-18T15:06:28.045Z' '@testing-library/dom@10.4.1': '2025-07-27T13:23:37.151Z' '@testing-library/jest-dom@6.9.1': '2025-10-01T20:04:22.720Z' - '@testing-library/react@16.3.2': '2026-01-19T10:59:08.185Z' + '@testing-library/react@16.3.3': '2026-08-27T17:41:18.735Z' '@testing-library/user-event@14.6.6': '2026-08-22T02:06:46.844Z' '@tsslint/cli@3.1.4': '2026-06-16T18:21:29.075Z' '@tsslint/compat-eslint@3.1.4': '2026-06-16T18:21:23.786Z' @@ -16971,9 +17075,9 @@ time: '@types/react-dom@19.2.5': '2026-08-23T21:05:23.671Z' '@types/react@19.2.18': '2026-07-30T21:54:03.456Z' '@types/sortablejs@1.15.9': '2025-10-24T04:31:45.132Z' - '@typescript-eslint/parser@8.67.0': '2026-08-10T17:22:16.082Z' + '@typescript-eslint/parser@8.69.0': '2026-08-31T17:09:13.713Z' '@typescript/typescript6@6.0.2': '2026-07-06T18:06:47.459Z' - '@vitejs/plugin-react@6.1.0': '2026-08-20T02:49:46.306Z' + '@vitejs/plugin-react@6.1.1': '2026-08-28T03:30:56.619Z' '@vitejs/plugin-rsc@0.5.34': '2026-08-07T07:38:06.175Z' '@vitest/browser-playwright@4.1.11': '2026-08-18T14:22:33.004Z' '@vitest/coverage-v8@4.1.11': '2026-08-18T14:22:18.896Z' @@ -17000,7 +17104,7 @@ time: embla-carousel-fade@8.6.0: '2025-04-04T17:37:50.278Z' embla-carousel-react@8.6.0: '2025-04-04T17:37:53.976Z' emoji-mart@5.6.0: '2024-04-25T14:22:21.440Z' - es-toolkit@1.51.0: '2026-08-17T02:49:29.399Z' + es-toolkit@1.52.0: '2026-08-28T07:22:17.687Z' eslint-markdown@0.12.1: '2026-07-10T23:35:26.055Z' eslint-plugin-antfu@3.2.3: '2026-05-11T02:24:38.348Z' eslint-plugin-better-tailwindcss@4.7.0: '2026-07-19T13:26:01.366Z' @@ -17011,21 +17115,21 @@ time: eslint-plugin-markdown-preferences@0.41.1: '2026-04-09T23:28:41.552Z' eslint-plugin-n@18.3.0: '2026-08-08T14:03:55.663Z' eslint-plugin-no-barrel-files@1.3.1: '2026-04-12T18:28:18.653Z' - eslint-plugin-perfectionist@5.10.1: '2026-08-04T05:40:14.988Z' + eslint-plugin-perfectionist@5.11.0: '2026-08-31T09:02:33.026Z' eslint-plugin-pnpm@1.8.0: '2026-08-13T06:26:51.536Z' eslint-plugin-regexp@3.1.1: '2026-06-25T23:07:43.989Z' eslint-plugin-storybook@10.5.10: '2026-08-20T10:46:43.128Z' eslint-plugin-toml@1.5.0: '2026-07-25T01:46:13.957Z' eslint-plugin-unicorn@71.1.0: '2026-07-06T22:07:48.695Z' eslint-plugin-yml@3.8.1: '2026-08-05T03:50:22.441Z' - eslint@10.9.0: '2026-08-21T13:07:47.620Z' + eslint@10.9.1: '2026-08-24T18:21:58.062Z' eventsource-parser@3.1.1: '2026-08-10T15:55:29.938Z' fast-deep-equal@3.1.3: '2020-06-08T07:27:28.474Z' - foxact@0.3.9: '2026-08-22T13:25:04.932Z' + foxact@0.3.10: '2026-08-27T16:19:48.187Z' fuse.js@7.5.0: '2026-07-13T17:23:36.705Z' - happy-dom@20.11.6: '2026-08-19T23:45:39.849Z' + happy-dom@20.12.0: '2026-08-29T16:12:16.364Z' hast-util-to-jsx-runtime@2.3.6: '2025-03-05T11:30:29.166Z' - hono@4.13.3: '2026-08-18T10:56:09.752Z' + hono@4.13.5: '2026-08-26T02:00:22.990Z' html-entities@2.6.0: '2025-03-30T15:40:10.885Z' html-to-image@1.11.13: '2025-02-14T01:43:48.709Z' i18next-resources-to-backend@1.2.3: '2026-07-31T15:07:13.021Z' @@ -17034,34 +17138,34 @@ time: immer@11.1.18: '2026-08-19T07:25:22.934Z' jotai-scope@0.11.0: '2026-05-13T18:43:15.331Z' jotai-tanstack-query@0.11.0: '2025-08-01T02:55:49.826Z' - jotai@2.20.2: '2026-07-14T13:52:11.083Z' + jotai@2.20.3: '2026-08-24T07:26:18.202Z' js-cookie@3.0.8: '2026-05-29T10:51:39.065Z' - js-yaml@5.3.0: '2026-08-14T09:31:39.879Z' + js-yaml@5.4.1: '2026-08-26T19:31:37.034Z' jsonschema@1.5.0: '2025-01-07T15:09:11.287Z' katex@0.17.0: '2026-05-22T08:06:26.967Z' - knip@6.32.2: '2026-08-11T20:34:19.949Z' - ky@2.0.2: '2026-04-21T08:58:46.923Z' + knip@6.34.0: '2026-08-31T22:54:33.744Z' + ky@2.1.0: '2026-08-28T13:10:48.983Z' lexical-code-no-prism@0.41.0: '2026-03-08T16:50:40.266Z' lexical@0.47.0: '2026-07-09T21:11:46.237Z' lockfile@1.0.4: '2018-04-17T00:36:12.565Z' - loro-crdt@1.14.1: '2026-08-10T11:39:06.910Z' - mediabunny@1.55.2: '2026-08-21T18:24:30.681Z' - mermaid@11.17.0: '2026-08-19T09:20:08.814Z' + loro-crdt@1.15.1: '2026-08-29T09:52:09.072Z' + mediabunny@1.55.5: '2026-08-31T12:20:23.255Z' + mermaid@11.17.2: '2026-08-25T11:52:39.930Z' mime@4.1.0: '2025-09-12T17:53:01.376Z' mitt@3.0.1: '2023-07-04T17:31:47.638Z' motion@12.43.0: '2026-07-28T14:29:31.291Z' negotiator@1.1.0: '2026-08-20T16:45:05.235Z' next-themes@0.4.6: '2025-03-11T21:02:05.882Z' - next@16.3.3: '2026-08-25T15:32:19.558Z' - nuqs@2.10.0: '2026-08-20T08:10:12.660Z' - open@11.0.1: '2026-08-13T23:39:43.829Z' + next@16.3.4: '2026-08-31T20:00:51.381Z' + nuqs@2.10.1: '2026-08-25T10:59:58.402Z' + open@11.0.2: '2026-08-29T13:12:01.838Z' ora@9.4.1: '2026-06-22T12:24:49.225Z' picocolors@1.1.1: '2024-10-16T18:20:03.921Z' pinyin-pro@3.29.3: '2026-08-19T01:06:36.944Z' playwright@1.62.1: '2026-07-30T16:38:49.134Z' postcss@8.5.26: '2026-08-06T08:33:00.043Z' qrcode.react@4.2.0: '2024-12-11T17:22:40.569Z' - qs@6.15.3: '2026-06-24T20:03:49.752Z' + qs@6.16.0: '2026-08-29T23:50:15.803Z' react-dom@19.2.8: '2026-07-21T15:41:41.267Z' react-easy-crop@6.2.3: '2026-07-24T14:14:31.126Z' react-i18next@17.0.12: '2026-08-20T10:15:43.090Z' @@ -17076,17 +17180,17 @@ time: remark-directive@4.0.0: '2025-02-27T15:15:20.630Z' scheduler@0.27.0: '2025-10-01T21:39:15.208Z' server-only@0.0.1: '2022-09-03T01:07:26.139Z' - shiki@4.4.2: '2026-08-05T00:41:14.729Z' + shiki@4.4.3: '2026-08-10T04:57:41.135Z' socket.io-client@4.8.3: '2025-12-23T16:39:16.428Z' sortablejs@1.15.7: '2026-02-11T22:42:31.720Z' std-semver@1.0.8: '2026-03-09T17:23:55.795Z' storybook@10.5.10: '2026-08-20T10:52:53.637Z' - streamdown@2.5.0: '2026-03-17T17:35:05.216Z' + streamdown@2.6.0: '2026-08-24T14:07:33.027Z' string-ts@2.3.1: '2025-11-28T17:33:10.099Z' tailwind-merge@3.6.0: '2026-05-10T12:56:43.142Z' tailwindcss@4.3.3: '2026-07-16T12:03:35.267Z' - tldts@7.4.10: '2026-07-30T23:11:08.153Z' - tsx@4.23.12: '2026-08-10T03:41:31.093Z' + tldts@7.4.11: '2026-08-24T07:31:22.093Z' + tsx@4.23.13: '2026-08-30T00:46:16.265Z' typescript@7.0.2: '2026-07-08T15:55:18.431Z' uglify-js@3.19.3: '2024-08-29T13:49:01.316Z' undici@7.29.0: '2026-07-24T12:52:58.701Z' @@ -17099,6 +17203,6 @@ time: vitest-browser-react@2.2.0: '2026-04-05T06:56:34.635Z' vitest-canvas-mock@1.1.5: '2026-08-13T07:03:06.784Z' vitest@4.1.11: '2026-08-18T14:27:07.240Z' - zod@4.4.3: '2026-05-04T07:06:40.819Z' + zod@4.5.4: '2026-08-29T17:55:42.775Z' zundo@2.3.0: '2024-11-17T16:35:11.372Z' zustand@5.0.15: '2026-08-13T00:39:55.466Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a2538f668af..28b40c1b9cc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -46,11 +46,11 @@ overrides: esbuild@>=0.27.3 <0.28.1: ^0.28.2 is-core-module: npm:@nolyfill/is-core-module@^1.0.39 js-yaml@<=4.1.1: ^4.1.2 - picomatch@>=4.0.0 <4.0.4: 4.0.5 + picomatch@>=4.0.0 <4.0.4: 4.0.7 postcss-selector-parser@>=6.0.0 <6.1.3: 6.1.4 postcss-selector-parser@>=7.0.0 <7.1.3: 7.1.5 postcss@<8.5.10: ^8.5.10 - rollup@>=4.0.0 <4.59.0: 4.62.5 + rollup@>=4.0.0 <4.59.0: 4.63.1 safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 side-channel: npm:@nolyfill/side-channel@^1.0.44 solid-js: 1.9.15 @@ -62,8 +62,8 @@ overrides: yaml@>=2.0.0 <2.8.3: 2.9.0 yauzl@<3.2.1: 3.2.1 catalog: - '@amplitude/analytics-browser': 2.45.6 - '@amplitude/plugin-session-replay-browser': 1.33.8 + '@amplitude/analytics-browser': 2.45.8 + '@amplitude/plugin-session-replay-browser': 1.35.0 '@axe-core/playwright': 4.13.0 '@base-ui/react': 1.7.0 '@chromatic-com/storybook': 5.3.0 @@ -87,7 +87,7 @@ catalog: '@lexical/selection': 0.47.0 '@lexical/text': 0.47.0 '@lexical/utils': 0.47.0 - '@mediabunny/mp3-encoder': 1.55.2 + '@mediabunny/mp3-encoder': 1.55.5 '@monaco-editor/react': 4.7.0 '@napi-rs/keyring': 1.3.0 '@orpc/client': 1.15.0 @@ -97,7 +97,7 @@ catalog: '@playwright/test': 1.62.1 '@remixicon/react': 4.9.0 '@rgrove/parse-xml': 4.2.3 - '@sentry/react': 10.70.0 + '@sentry/react': 10.73.0 '@storybook/addon-a11y': 10.5.10 '@storybook/addon-docs': 10.5.10 '@storybook/addon-links': 10.5.10 @@ -113,16 +113,16 @@ catalog: '@t3-oss/env-nextjs': 0.13.11 '@tailwindcss/postcss': 4.3.3 '@tailwindcss/vite': 4.3.3 - '@tanstack/eslint-plugin-query': 5.102.2 + '@tanstack/eslint-plugin-query': 5.102.8 '@tanstack/form-core': 1.33.5 - '@tanstack/query-core': 5.102.2 + '@tanstack/query-core': 5.102.8 '@tanstack/react-form': 1.33.5 '@tanstack/react-hotkeys': 0.10.0 - '@tanstack/react-query': 5.102.2 + '@tanstack/react-query': 5.102.8 '@tanstack/react-virtual': 3.14.10 '@testing-library/dom': 10.4.1 '@testing-library/jest-dom': 6.9.1 - '@testing-library/react': 16.3.2 + '@testing-library/react': 16.3.3 '@testing-library/user-event': 14.6.6 '@tsslint/cli': 3.1.4 '@tsslint/compat-eslint': 3.1.4 @@ -135,9 +135,9 @@ catalog: '@types/react': 19.2.18 '@types/react-dom': 19.2.5 '@types/sortablejs': 1.15.9 - '@typescript-eslint/parser': 8.67.0 + '@typescript-eslint/parser': 8.69.0 '@typescript/native': npm:typescript@7.0.2 - '@vitejs/plugin-react': 6.1.0 + '@vitejs/plugin-react': 6.1.1 '@vitejs/plugin-rsc': 0.5.34 '@vitest/browser-playwright': 4.1.11 '@vitest/coverage-v8': 4.1.11 @@ -163,8 +163,8 @@ catalog: embla-carousel-fade: 8.6.0 embla-carousel-react: 8.6.0 emoji-mart: 5.6.0 - es-toolkit: 1.51.0 - eslint: 10.9.0 + es-toolkit: 1.52.0 + eslint: 10.9.1 eslint-markdown: 0.12.1 eslint-plugin-antfu: 3.2.3 eslint-plugin-better-tailwindcss: 4.7.0 @@ -175,7 +175,7 @@ catalog: eslint-plugin-markdown-preferences: 0.41.1 eslint-plugin-n: 18.3.0 eslint-plugin-no-barrel-files: 1.3.1 - eslint-plugin-perfectionist: 5.10.1 + eslint-plugin-perfectionist: 5.11.0 eslint-plugin-pnpm: 1.8.0 eslint-plugin-regexp: 3.1.1 eslint-plugin-storybook: 10.5.10 @@ -184,46 +184,46 @@ catalog: eslint-plugin-yml: 3.8.1 eventsource-parser: 3.1.1 fast-deep-equal: 3.1.3 - foxact: 0.3.9 + foxact: 0.3.10 fuse.js: 7.5.0 - happy-dom: 20.11.6 + happy-dom: 20.12.0 hast-util-to-jsx-runtime: 2.3.6 - hono: 4.13.3 + hono: 4.13.5 html-entities: 2.6.0 html-to-image: 1.11.13 i18next: 26.4.0 i18next-resources-to-backend: 1.2.3 iconify-import-svg: 0.2.0 immer: 11.1.18 - jotai: 2.20.2 + jotai: 2.20.3 jotai-scope: 0.11.0 jotai-tanstack-query: 0.11.0 js-cookie: 3.0.8 - js-yaml: 5.3.0 + js-yaml: 5.4.1 jsonschema: 1.5.0 katex: 0.17.0 - knip: 6.32.2 - ky: 2.0.2 + knip: 6.34.0 + ky: 2.1.0 lexical: 0.47.0 lockfile: 1.0.4 - loro-crdt: 1.14.1 - mediabunny: 1.55.2 - mermaid: 11.17.0 + loro-crdt: 1.15.1 + mediabunny: 1.55.5 + mermaid: 11.17.2 mime: 4.1.0 mitt: 3.0.1 motion: 12.43.0 negotiator: 1.1.0 - next: 16.3.3 + next: 16.3.4 next-themes: 0.4.6 - nuqs: 2.10.0 - open: 11.0.1 + nuqs: 2.10.1 + open: 11.0.2 ora: 9.4.1 picocolors: 1.1.1 pinyin-pro: 3.29.3 playwright: 1.62.1 postcss: 8.5.26 qrcode.react: 4.2.0 - qs: 6.15.3 + qs: 6.16.0 react: 19.2.8 react-dom: 19.2.8 react-easy-crop: 6.2.3 @@ -238,17 +238,17 @@ catalog: remark-directive: 4.0.0 scheduler: 0.27.0 server-only: 0.0.1 - shiki: 4.4.2 + shiki: 4.4.3 socket.io-client: 4.8.3 sortablejs: 1.15.7 std-semver: 1.0.8 storybook: 10.5.10 - streamdown: 2.5.0 + streamdown: 2.6.0 string-ts: 2.3.1 tailwind-merge: 3.6.0 tailwindcss: 4.3.3 - tldts: 7.4.10 - tsx: 4.23.12 + tldts: 7.4.11 + tsx: 4.23.13 typescript: npm:@typescript/typescript6@6.0.2 uglify-js: 3.19.3 undici: 7.29.0 @@ -262,7 +262,7 @@ catalog: vitest: 4.1.11 vitest-browser-react: 2.2.0 vitest-canvas-mock: 1.1.5 - zod: 4.4.3 + zod: 4.5.4 zundo: 2.3.0 zustand: 5.0.15 peerDependencyRules: diff --git a/web/Dockerfile b/web/Dockerfile index 9ddb06b8ed5..b60983d5665 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,5 +1,6 @@ # base image -FROM node:22.22.1-alpine AS base +ARG NODE_VERSION=24.20.0 +FROM node:${NODE_VERSION}-alpine AS base LABEL maintainer="takatost@gmail.com" # if you located in China, you can use aliyun mirror to speed up diff --git a/web/Dockerfile.dockerignore b/web/Dockerfile.dockerignore index 115f4303fa9..4a75679e91c 100644 --- a/web/Dockerfile.dockerignore +++ b/web/Dockerfile.dockerignore @@ -2,7 +2,6 @@ !package.json !pnpm-lock.yaml !pnpm-workspace.yaml -!.nvmrc !web/ !web/** !e2e/ diff --git a/web/README.md b/web/README.md index cd4cc161b1e..cb5abb83bc4 100644 --- a/web/README.md +++ b/web/README.md @@ -6,7 +6,7 @@ This is a [Next.js] application with [vinext] as the default local development s ### Run by source code -The required Node.js and pnpm versions are pinned by the repository root `.nvmrc` and `packageManager` field. [Vite+] is also available for repository checks and tests; use its official documentation as the installation reference. +The required Node.js and pnpm versions are pinned by the repository root `devEngines.runtime` and `packageManager` fields. [Vite+] is also available for repository checks and tests; use its official documentation as the installation reference. - [Node.js] - [pnpm] @@ -20,7 +20,7 @@ pnpm install ``` > [!NOTE] -> JavaScript dependencies are managed by the workspace files at the repository root: `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, and `.nvmrc`. +> JavaScript dependencies are managed by the workspace files at the repository root: `package.json`, `pnpm-lock.yaml`, and `pnpm-workspace.yaml`. > Install dependencies and run the commands below from the repository root. Then, configure the environment variables. diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx index 9a52e32d0e7..8c6acc78866 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx @@ -238,11 +238,9 @@ describe('AppDetailLayout', () => { expect(useStore.getState().appDetail?.id).toBe('app-1') }) - it('should allow users with access point permission to open access point directly', async () => { + it('should allow access point pages without app deploy or app ACL permissions', async () => { mockPathname = '/app/app-1/access-point' - mockFetchAppDetailDirect.mockResolvedValue( - createAppDetail({ permission_keys: [AppACLPermission.AccessPoint] }), - ) + mockFetchAppDetailDirect.mockResolvedValue(createAppDetail({ permission_keys: [] })) render( @@ -256,44 +254,6 @@ describe('AppDetailLayout', () => { expect(useStore.getState().appDetail?.id).toBe('app-1') }) - it('should redirect access point pages when access point permission is missing', async () => { - mockPathname = '/app/app-1/access-point' - mockFetchAppDetailDirect.mockResolvedValue( - createAppDetail({ permission_keys: [AppACLPermission.Monitor] }), - ) - - render( - -
App page content
-
, - ) - - await waitFor(() => { - expect(mockReplace).toHaveBeenCalledWith('/app/app-1/overview') - }) - expect(screen.queryByText('App page content')).not.toBeInTheDocument() - expect(useStore.getState().appDetail).toBeUndefined() - }) - - it('should keep access point content hidden while redirecting cached app data without permission', async () => { - mockPathname = '/app/app-1/access-point' - useStore - .getState() - .setAppDetail(createAppDetail({ permission_keys: [AppACLPermission.Monitor] })) - - render( - -
App page content
-
, - ) - - expect(screen.queryByText('App page content')).not.toBeInTheDocument() - await waitFor(() => { - expect(mockReplace).toHaveBeenCalledWith('/app/app-1/overview') - }) - expect(mockFetchAppDetailDirect).not.toHaveBeenCalled() - }) - it('should redirect deploy pages when app deploy ACL permission is missing', async () => { mockPathname = '/app/app-1/deploy' mockFetchAppDetailDirect.mockResolvedValue( @@ -357,7 +317,7 @@ describe('AppDetailLayout', () => { ) await waitFor(() => { - expect(mockReplace).toHaveBeenCalledWith('/apps') + expect(mockReplace).toHaveBeenCalledWith('/app/app-1/access-point') }) expect(screen.queryByText('App page content')).not.toBeInTheDocument() expect(useStore.getState().appDetail).toBeUndefined() @@ -528,7 +488,7 @@ describe('AppDetailLayout', () => { ) await waitFor(() => { - expect(mockReplace).toHaveBeenCalledWith('/apps') + expect(mockReplace).toHaveBeenCalledWith('/app/app-1/access-point') }) expect(screen.queryByText('App page content')).not.toBeInTheDocument() expect(useStore.getState().appDetail).toBeUndefined() diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index df55021f289..7f9f6973ca8 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -78,28 +78,8 @@ const AppDetailLayout: FC = (props) => { appDetail?.id === appId ? appDetail : appDetailRes?.id === appId ? appDetailRes : null const pageTitle = appDetailPageTitle(pathname, t) const appName = routeAppDetail?.id === appId ? routeAppDetail.name : undefined - const isAppACLContextReady = - !!routeAppDetail && - !!currentWorkspace.id && - !isLoadingCurrentWorkspace && - !isLoadingWorkspacePermissionKeys && - !isLoadingAppDetail - const appACLCapabilities = React.useMemo( - () => - routeAppDetail && isAppACLContextReady - ? getAppACLCapabilities(routeAppDetail.permission_keys, { - currentUserId, - resourceMaintainer: routeAppDetail.maintainer, - workspacePermissionKeys, - isRbacEnabled, - }) - : null, - [currentUserId, isAppACLContextReady, isRbacEnabled, routeAppDetail, workspacePermissionKeys], - ) const shouldBlockAgentResourceAccess = routeAppDetail?.mode === AppModeEnum.AGENT && pathname.endsWith('/access-config') - const shouldBlockAccessPointAccess = - pathname.endsWith('/access-point') && !appACLCapabilities?.canAccessPoint useDocumentTitle(`${pageTitle} · ${appName || t(($) => $['menus.appDetail'], { ns: 'common' })}`) @@ -140,16 +120,28 @@ const AppDetailLayout: FC = (props) => { }, [appId, router, setAppDetail]) useEffect(() => { - if (!routeAppDetail || !isAppACLContextReady || !appACLCapabilities) return + if ( + !routeAppDetail || + !currentWorkspace.id || + isLoadingCurrentWorkspace || + isLoadingWorkspacePermissionKeys || + isLoadingAppDetail + ) + return if (routeAppDetail.id !== appId) return + const appACLCapabilities = getAppACLCapabilities(routeAppDetail.permission_keys, { + currentUserId, + resourceMaintainer: routeAppDetail.maintainer, + workspacePermissionKeys, + isRbacEnabled, + }) const isLayoutPath = pathname.endsWith('configuration') || pathname.endsWith('workflow') const isLogsPath = pathname.endsWith('logs') const isAnnotationsPath = pathname.endsWith('annotations') const isOverviewPath = pathname.endsWith('overview') const isAccessConfigPath = pathname.endsWith('access-config') const isDeployPath = pathname.endsWith('deploy') - const isAccessPointPath = pathname.endsWith('access-point') if ( (isLayoutPath && !appACLCapabilities.canAccessLayout) || (isLogsPath && !appACLCapabilities.canAccessLogAndAnnotation) || @@ -158,8 +150,7 @@ const AppDetailLayout: FC = (props) => { (isAccessConfigPath && (routeAppDetail.mode === AppModeEnum.AGENT || !appACLCapabilities.canAccessConfig)) || (isDeployPath && - (routeAppDetail.mode !== AppModeEnum.WORKFLOW || !appACLCapabilities.canDeploy)) || - (isAccessPointPath && !appACLCapabilities.canAccessPoint) + (routeAppDetail.mode !== AppModeEnum.WORKFLOW || !appACLCapabilities.canDeploy)) ) { router.replace( getRedirectionPath(routeAppDetail, { @@ -189,12 +180,14 @@ const AppDetailLayout: FC = (props) => { if (appDetailRes && appDetail?.id !== appDetailRes.id) setAppDetail({ ...appDetailRes, enable_sso: false }) }, [ - appACLCapabilities, appDetail?.id, appDetailRes, appId, currentUserId, - isAppACLContextReady, + currentWorkspace.id, + isLoadingAppDetail, + isLoadingCurrentWorkspace, + isLoadingWorkspacePermissionKeys, isRbacEnabled, pathname, routeAppDetail, @@ -205,7 +198,7 @@ const AppDetailLayout: FC = (props) => { const isWorkflowPage = pathname.endsWith('/workflow') const content = - !appDetail || shouldBlockAgentResourceAccess || shouldBlockAccessPointAccess ? ( + !appDetail || shouldBlockAgentResourceAccess ? (
diff --git a/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx b/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx index 0ff08042648..e84d925dbdc 100644 --- a/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx +++ b/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx @@ -186,10 +186,7 @@ describe('AppDetailSection', () => { ).not.toBeInTheDocument() }) - it('should render access point navigation when access point permission is granted', () => { - // Arrange - mockAppPermissionKeys = [AppACLPermission.AccessPoint] - + it('should render access point navigation using its app route', () => { // Act render() @@ -203,19 +200,6 @@ describe('AppDetailSection', () => { ).not.toBeInTheDocument() }) - it('should hide access point navigation when access point permission is missing', () => { - // Arrange - mockAppPermissionKeys = [AppACLPermission.Monitor] - - // Act - render() - - // Assert - expect( - screen.queryByRole('link', { name: 'common.appMenus.accessPoint' }), - ).not.toBeInTheDocument() - }) - it('should render deploy navigation with app deploy ACL regardless of the legacy workspace role', () => { // Arrange mockAppMode = 'workflow' diff --git a/web/app/components/app-sidebar/app-detail-section.tsx b/web/app/components/app-sidebar/app-detail-section.tsx index 677ac50aea4..3176e2cac21 100644 --- a/web/app/components/app-sidebar/app-detail-section.tsx +++ b/web/app/components/app-sidebar/app-detail-section.tsx @@ -120,16 +120,12 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => { }, ] : []), - ...(appACLCapabilities.canAccessPoint - ? [ - { - name: t(($) => $['appMenus.accessPoint'], { ns: 'common' }), - href: `/app/${appId}/access-point`, - icon: accessPointNavIcon, - selectedIcon: accessPointNavIcon, - }, - ] - : []), + { + name: t(($) => $['appMenus.accessPoint'], { ns: 'common' }), + href: `/app/${appId}/access-point`, + icon: accessPointNavIcon, + selectedIcon: accessPointNavIcon, + }, ...(supportsAppDeploy && appACLCapabilities.canDeploy ? [ { diff --git a/web/app/components/app/access-point/__tests__/access-point-card.spec.tsx b/web/app/components/app/access-point/__tests__/access-point-card.spec.tsx index 5f740babef4..c800deded33 100644 --- a/web/app/components/app/access-point/__tests__/access-point-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/access-point-card.spec.tsx @@ -21,6 +21,11 @@ describe('AccessPointCard', () => { 'data-highlighted', 'true', ) + expect(screen.getByRole('heading', { name: 'Web App' })).toHaveAttribute('title', 'Web App') + expect(screen.getByText('Web application access')).toHaveAttribute( + 'title', + 'Web application access', + ) }) it.each<[AccessPointStatus, string, boolean]>([ diff --git a/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx b/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx index 5cefe0aeeb7..b88bb755384 100644 --- a/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx +++ b/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx @@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({ } as Record, workflow: { data: null as Record | null, + isError: false, isPending: false, }, webCard: vi.fn(), @@ -23,6 +24,7 @@ const mocks = vi.hoisted(() => ({ }, mcpCard: vi.fn(), triggerCard: vi.fn(), + useAppWorkflow: vi.fn(), })) vi.mock('react-i18next', async () => { @@ -60,7 +62,10 @@ vi.mock('@/context/i18n', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useAppWorkflow: () => mocks.workflow, + useAppWorkflow: (...args: unknown[]) => { + mocks.useAppWorkflow(...args) + return mocks.workflow + }, })) vi.mock('@/utils/permission', () => ({ @@ -118,6 +123,7 @@ describe('BuiltInAccessPoints', () => { } mocks.workflow = { data: null, + isError: false, isPending: false, } mocks.capabilities = { @@ -150,6 +156,7 @@ describe('BuiltInAccessPoints', () => { nodes: [{ data: { type: 'start' } }], }, }, + isError: false, isPending: false, } @@ -197,6 +204,7 @@ describe('BuiltInAccessPoints', () => { nodes: [{ data: { type: 'trigger-webhook' } }], }, }, + isError: false, isPending: false, } @@ -222,6 +230,7 @@ describe('BuiltInAccessPoints', () => { it('keeps all cards visible while the published workflow is loading', () => { mocks.workflow = { data: null, + isError: false, isPending: true, } @@ -233,4 +242,30 @@ describe('BuiltInAccessPoints', () => { expect.objectContaining({ availability: 'loading' }), ) }) + + it('does not show the unpublished card when loading the published workflow fails', () => { + mocks.workflow = { + data: null, + isError: true, + isPending: false, + } + + render() + + expect( + screen.queryByText('deployments.studio.accessPoint.noPublishedTitle'), + ).not.toBeInTheDocument() + }) + + it('does not retry forbidden published workflow requests', () => { + render() + + const options = mocks.useAppWorkflow.mock.calls.at(-1)?.[1] as { + retry: (failureCount: number, error: unknown) => boolean + } + + expect(options.retry(0, new Response(null, { status: 403 }))).toBe(false) + expect(options.retry(0, new Response(null, { status: 500 }))).toBe(true) + expect(options.retry(3, new Response(null, { status: 500 }))).toBe(false) + }) }) diff --git a/web/app/components/app/access-point/built-in-access-points/index.tsx b/web/app/components/app/access-point/built-in-access-points/index.tsx index 95847013f29..5f739e69ea2 100644 --- a/web/app/components/app/access-point/built-in-access-points/index.tsx +++ b/web/app/components/app/access-point/built-in-access-points/index.tsx @@ -39,9 +39,17 @@ export function BuiltInAccessPoints({ appId, highlightedAccessPoint }: BuiltInAc const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const shouldFetchWorkflow = Boolean(appInfo && isAdvancedApp(appInfo)) - const { data: workflow, isPending: workflowLoading } = useAppWorkflow( - shouldFetchWorkflow ? appId : '', - ) + const { + data: workflow, + isError: workflowError, + isPending: workflowLoading, + } = useAppWorkflow(shouldFetchWorkflow ? appId : '', { + retry: (failureCount, error) => { + if (error instanceof Response && error.status === 403) return false + + return failureCount < 3 + }, + }) const capabilities = useMemo( () => getAppACLCapabilities(appInfo?.permission_keys, { @@ -72,7 +80,7 @@ export function BuiltInAccessPoints({ appId, highlightedAccessPoint }: BuiltInAc return (
- {workflowState.isUnpublished && !workflowLoading && ( + {workflowState.isUnpublished && !workflowLoading && !workflowError && (
diff --git a/web/app/components/app/access-point/shared/access-point-card.tsx b/web/app/components/app/access-point/shared/access-point-card.tsx index 664b21a0500..3e4430dea85 100644 --- a/web/app/components/app/access-point/shared/access-point-card.tsx +++ b/web/app/components/app/access-point/shared/access-point-card.tsx @@ -72,10 +72,12 @@ export function AccessPointCard({ icon )} -

+

{title}

- {description} + + {description} +
{showStatus && ( <> diff --git a/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx b/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx index 5cbd69dfc6a..48943656d07 100644 --- a/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx +++ b/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx @@ -26,7 +26,10 @@ const ClearAllAnnotationsConfirmModal: FC = ({ isShow, onHide, onConfirm !open && onHide()}>
- + {title}
diff --git a/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx b/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx index 55e347dfdfa..dbaa091659f 100644 --- a/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx +++ b/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx @@ -25,7 +25,10 @@ const RemoveAnnotationConfirmModal: FC = ({ isShow, onHide, onRemove }) = !open && onHide()}>
- + {title}
diff --git a/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx b/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx index 9c80ad25f11..8aa12ed5457 100644 --- a/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx @@ -295,7 +295,6 @@ function renderFlow( return render( ({ + @@ -107,6 +113,23 @@ describe('FeaturesWrappedAppPublisher', () => { }) }) + it('should pass publish notification options through to onPublish', async () => { + render( + , + ) + + fireEvent.click(screen.getByText('publish-silently-through-wrapper')) + + await waitFor(() => { + expect(mockOnPublish).toHaveBeenCalledWith(undefined, mockFeatures, { + showSuccessToast: false, + }) + }) + }) + it('should restore published features after confirmation', async () => { render( ({ })) const mockPublishToCreatorsPlatform = vi.fn() +const mockCreateWorkflowToolProvider = vi.fn() vi.mock('@/service/apps', () => ({ publishToCreatorsPlatform: (...args: unknown[]) => mockPublishToCreatorsPlatform(...args), })) +vi.mock('@/service/tools', () => ({ + createWorkflowToolProvider: (...args: unknown[]) => mockCreateWorkflowToolProvider(...args), + saveWorkflowToolProvider: vi.fn(), +})) + vi.mock('@/service/use-workflow', () => ({ useAppWorkflow: () => ({ data: mockPublishedWorkflow, @@ -167,9 +173,26 @@ vi.mock('@/app/components/base/amplitude', () => ({ })) vi.mock('@/app/components/tools/workflow-tool', () => ({ - WorkflowToolDrawer: ({ onHide }: { onHide: () => void }) => ( + WorkflowToolDrawer: ({ + onCreate, + onHide, + }: { + onCreate?: (payload: Record) => void + onHide: () => void + }) => (
workflow tool drawer + @@ -818,6 +841,74 @@ describe('AppPublisher', () => { expect(screen.getByRole('dialog', { name: 'Workflow tool drawer' })).toBeInTheDocument() }) + it('should show one success toast when automatically publishing a workflow tool', async () => { + mockAppDetail = { + ...mockAppDetail, + mode: AppModeEnum.WORKFLOW, + } + mockOnPublish.mockImplementation(async (_params, options?: { showSuccessToast?: boolean }) => { + if (options?.showSuccessToast !== false) mockToastSuccess('common.api.actionSuccess') + }) + mockCreateWorkflowToolProvider.mockResolvedValue({}) + + render() + + fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/)) + fireEvent.click(screen.getByText('publisher-workflow-tool')) + fireEvent.click(screen.getByRole('button', { name: 'create-workflow-tool' })) + + await waitFor(() => { + expect(mockCreateWorkflowToolProvider).toHaveBeenCalledOnce() + }) + expect(mockOnPublish).toHaveBeenCalledWith(undefined, { showSuccessToast: false }) + expect(mockToastSuccess).toHaveBeenCalledOnce() + }) + + it('should not show a success toast when workflow tool creation fails after publishing', async () => { + mockAppDetail = { + ...mockAppDetail, + mode: AppModeEnum.WORKFLOW, + } + mockOnPublish.mockImplementation(async (_params, options?: { showSuccessToast?: boolean }) => { + if (options?.showSuccessToast !== false) mockToastSuccess('common.api.actionSuccess') + }) + mockCreateWorkflowToolProvider.mockRejectedValue(new Error('create failed')) + + render() + + fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/)) + fireEvent.click(screen.getByText('publisher-workflow-tool')) + fireEvent.click(screen.getByRole('button', { name: 'create-workflow-tool' })) + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith('create failed') + }) + expect(mockOnPublish).toHaveBeenCalledWith(undefined, { showSuccessToast: false }) + expect(mockToastSuccess).not.toHaveBeenCalled() + }) + + it('should not create a workflow tool when automatic publishing fails', async () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + mockAppDetail = { + ...mockAppDetail, + mode: AppModeEnum.WORKFLOW, + } + mockOnPublish.mockRejectedValueOnce(new Error('publish failed')) + + render() + + fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/)) + fireEvent.click(screen.getByText('publisher-workflow-tool')) + fireEvent.click(screen.getByRole('button', { name: 'create-workflow-tool' })) + + await waitFor(() => { + expect(mockOnPublish).toHaveBeenCalledOnce() + }) + expect(mockCreateWorkflowToolProvider).not.toHaveBeenCalled() + expect(mockToastError).toHaveBeenCalledWith('publish failed') + consoleWarnSpy.mockRestore() + }) + it('should not open workflow tool drawer without tool.manage', () => { mockWorkspacePermissionKeys = [] mockAppDetail = { diff --git a/web/app/components/app/app-publisher/__tests__/sections.spec.tsx b/web/app/components/app/app-publisher/__tests__/sections.spec.tsx index 11c0b1daf3f..9090a86bbd4 100644 --- a/web/app/components/app/app-publisher/__tests__/sections.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/sections.spec.tsx @@ -6,7 +6,6 @@ import { renderWithConsoleQuery as render } from '@/test/console/query-data' import { AppModeEnum } from '@/types/app' import { PublisherActionsSection } from '../built-in-publisher/actions-section' import { PublisherSummarySection } from '../built-in-publisher/summary-section' -import { PublisherEnvironmentActionsSection } from '../environment-deployment-flow/actions-section' vi.mock('../publish-with-multiple-model', () => ({ default: ({ @@ -315,7 +314,6 @@ describe('app-publisher sections', () => { description: 'Workflow description', }} appURL="https://example.com/app" - canAccessPoint disabledFunctionButton={false} disabledFunctionTooltip="disabled" handleOpenRunConfig={handleOpenRunConfig} @@ -496,7 +494,6 @@ describe('app-publisher sections', () => { mode: AppModeEnum.WORKFLOW, }} appURL="https://example.com/app" - canAccessPoint disabledFunctionButton={false} hasHumanInputNode={false} hasTriggerNode @@ -520,49 +517,11 @@ describe('app-publisher sections', () => { ) }) - it('should hide the built-in Access Point action without permission', () => { - render( - , - ) - - expect(screen.queryByText(/(?:^|\.)appMenus\.accessPoint(?=$|:)/)).not.toBeInTheDocument() - expect(screen.getByRole('link', { name: /appMenus\.deploy\b/ })).toHaveAttribute( - 'href', - '/app/workflow-app/deploy', - ) - }) - - it('should hide the environment Access Point action without permission', () => { - render( - , - ) - - expect(screen.queryByText(/(?:^|\.)appMenus\.accessPoint(?=$|:)/)).not.toBeInTheDocument() - expect(screen.getByText(/(?:^|\.)appMenus\.deploy(?=$|:)/)).toBeInTheDocument() - }) - it('should expose unavailable quick links as disabled buttons before the first publish', () => { render( void @@ -42,7 +41,6 @@ type PublisherActionsSectionProps = Pick< export function PublisherActionsSection({ appDetail, appURL, - canAccessPoint = false, disabledFunctionButton, disabledFunctionTooltip, handleOpenRunConfig, @@ -116,16 +114,14 @@ export function PublisherActionsSection({ {disabledFunctionTooltip} )} - {canAccessPoint && ( - $['common.accessPointDescription'], { ns: 'workflow' })} - link={appId ? `/app/${appId}/access-point` : undefined} - icon={} - > - {t(($) => $['appMenus.accessPoint'], { ns: 'common' })} - - )} + $['common.accessPointDescription'], { ns: 'workflow' })} + link={appId ? `/app/${appId}/access-point` : undefined} + icon={} + > + {t(($) => $['appMenus.accessPoint'], { ns: 'common' })} + {showDeploy && ( - {canAccessPoint && ( - $['common.accessPointDescription'], { ns: 'workflow' })} - link={accessPointHref} - icon={} - > - {t(($) => $['appMenus.accessPoint'], { ns: 'common' })} - - )} + $['common.accessPointDescription'], { ns: 'workflow' })} + link={accessPointHref} + icon={} + > + {t(($) => $['appMenus.accessPoint'], { ns: 'common' })} + $['common.deployDescription'], { ns: 'workflow' })} diff --git a/web/app/components/app/app-publisher/environment-deployment-flow/index.tsx b/web/app/components/app/app-publisher/environment-deployment-flow/index.tsx index 9f0b1201993..dce08832bc0 100644 --- a/web/app/components/app/app-publisher/environment-deployment-flow/index.tsx +++ b/web/app/components/app/app-publisher/environment-deployment-flow/index.tsx @@ -15,7 +15,6 @@ import { PublisherEnvironmentSummarySection } from './summary-section' type PublisherEnvironmentFlowProps = { appId?: string - canAccessPoint?: boolean deployment?: EnvironmentDeployment environmentId: string environmentName: string @@ -29,7 +28,6 @@ type PublisherEnvironmentFlowProps = { export function PublisherEnvironmentFlow({ appId, - canAccessPoint = false, deployment, environmentId, environmentName, @@ -93,7 +91,6 @@ export function PublisherEnvironmentFlow({ /> diff --git a/web/app/components/app/app-publisher/features-wrapper.tsx b/web/app/components/app/app-publisher/features-wrapper.tsx index 4114aadb50a..ffb85da6b44 100644 --- a/web/app/components/app/app-publisher/features-wrapper.tsx +++ b/web/app/components/app/app-publisher/features-wrapper.tsx @@ -1,5 +1,6 @@ import type { AppPublisherProps, + AppPublisherPublishOptions, AppPublisherPublishParams, } from '@/app/components/app/app-publisher/types' import type { ConfigurationPublishConfig } from '@/app/components/app/configuration/hooks/configuration-lifecycle/types' @@ -26,6 +27,7 @@ type Props = Omit & { onPublish?: ( params?: AppPublisherPublishParams, features?: Features, + options?: AppPublisherPublishOptions, ) => Promise | unknown publishedConfig: ConfigurationPublishConfig resetAppConfig?: () => void @@ -88,7 +90,8 @@ const FeaturesWrappedAppPublisher = (props: Props) => { } const handlePublish = useCallback( - (params?: AppPublisherPublishParams) => { + (params?: AppPublisherPublishParams, options?: AppPublisherPublishOptions) => { + if (options) return props.onPublish?.(params, features, options) return props.onPublish?.(params, features) }, [features, props], diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 38e9b724d1e..48278b77cfc 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -17,13 +17,12 @@ export function AppPublisher(props: AppPublisherProps) { select: (data) => data.profile.id, }) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) - const appACLCapabilities = getAppACLCapabilities(appDetail?.permission_keys, { + const canDeploy = getAppACLCapabilities(appDetail?.permission_keys, { currentUserId, resourceMaintainer: appDetail?.maintainer, workspacePermissionKeys, - }) - const supportsMultiEnvironment = - appDetail?.mode === AppModeEnum.WORKFLOW && appACLCapabilities.canDeploy + }).canDeploy + const supportsMultiEnvironment = appDetail?.mode === AppModeEnum.WORKFLOW && canDeploy return ( void } export function PublisherContent({ - canAccessPoint, crossAxisOffset = 0, debugWithMultipleModel = false, disabled = false, @@ -132,7 +130,7 @@ export function PublisherContent({ hasTriggerNode, inputs, onClosePublisher: closePublisher, - onPublish: publish.handlePublish, + onPublish: publish.publishWorkflowTool, onRefreshData, outputs, toolPublished, @@ -214,7 +212,6 @@ export function PublisherContent({ actions: { appDetail, appURL, - canAccessPoint, disabledFunctionButton, disabledFunctionTooltip, handleOpenRunConfig: workflowLaunch.openDialog, @@ -239,7 +236,6 @@ export function PublisherContent({ disabled={disabled} environmentPublisher={{ appId: appDetail?.id, - canAccessPoint, deployment: selectedEnvironmentDeployment, environmentId: selectedEnvironmentId, environmentName: diff --git a/web/app/components/app/app-publisher/publisher-content/use-publish-controller.ts b/web/app/components/app/app-publisher/publisher-content/use-publish-controller.ts index 0fc2f7acb3d..6737b74ee17 100644 --- a/web/app/components/app/app-publisher/publisher-content/use-publish-controller.ts +++ b/web/app/components/app/app-publisher/publisher-content/use-publish-controller.ts @@ -1,5 +1,9 @@ import type { QueryClient } from '@tanstack/react-query' -import type { AppPublisherProps, AppPublisherPublishParams } from '../types' +import type { + AppPublisherProps, + AppPublisherPublishOptions, + AppPublisherPublishParams, +} from '../types' import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration' import { useHotkey } from '@tanstack/react-hotkeys' import { useQueryClient } from '@tanstack/react-query' @@ -81,43 +85,54 @@ export function usePublishController({ : publishedAt const hasPublishedVersion = Boolean(currentPublishedAt) + async function publishApp( + params?: AppPublisherPublishParams, + options?: AppPublisherPublishOptions, + ) { + await onPublish?.(params, options) + setPublished(true) + + const socket = appId ? webSocketClient.getSocket(appId) : null + if (appId) { + invalidateAppWorkflow(appId) + if (supportsMultiEnvironment) refreshAppDeploymentData(queryClient, appId) + } else { + console.warn('[app-publisher] missing appId, skip workflow invalidate and socket emit') + } + if (socket) { + const timestamp = Date.now() + socket.emit('collaboration_event', { + type: 'app_publish_update', + data: { + action: 'published', + timestamp, + }, + timestamp, + }) + } else if (appId) { + console.warn('[app-publisher] socket not ready, skip collaboration_event emit', { appId }) + } + + trackEvent('app_published_time', { + action_mode: 'app', + app_id: appId, + app_name: appName, + }) + } + async function handlePublish(params?: AppPublisherPublishParams) { try { - await onPublish?.(params) - setPublished(true) - - const socket = appId ? webSocketClient.getSocket(appId) : null - if (appId) { - invalidateAppWorkflow(appId) - if (supportsMultiEnvironment) refreshAppDeploymentData(queryClient, appId) - } else { - console.warn('[app-publisher] missing appId, skip workflow invalidate and socket emit') - } - if (socket) { - const timestamp = Date.now() - socket.emit('collaboration_event', { - type: 'app_publish_update', - data: { - action: 'published', - timestamp, - }, - timestamp, - }) - } else if (appId) { - console.warn('[app-publisher] socket not ready, skip collaboration_event emit', { appId }) - } - - trackEvent('app_published_time', { - action_mode: 'app', - app_id: appId, - app_name: appName, - }) + await publishApp(params) } catch (error) { console.warn('[app-publisher] publish failed', error) setPublished(false) } } + async function publishWorkflowTool(params?: AppPublisherPublishParams) { + await publishApp(params, { showSuccessToast: false }) + } + async function handleRestore() { try { await onRestore?.() @@ -164,6 +179,7 @@ export function usePublishController({ isWorkflowApp, published, publishedWorkflow, + publishWorkflowTool, resetPublished: () => setPublished(false), } } diff --git a/web/app/components/app/app-publisher/publisher-content/use-workflow-tool.ts b/web/app/components/app/app-publisher/publisher-content/use-workflow-tool.ts index d79b33cf4bb..01609e2ef8a 100644 --- a/web/app/components/app/app-publisher/publisher-content/use-workflow-tool.ts +++ b/web/app/components/app/app-publisher/publisher-content/use-workflow-tool.ts @@ -1,5 +1,6 @@ import type { AppPublisherPublishParams } from '../types' -import type { InputVar, Variable } from '@/app/components/workflow/types' +import type { WorkflowToolOutputVariable } from '@/app/components/tools/types' +import type { InputVar } from '@/app/components/workflow/types' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useCanManageTools } from '@/app/components/tools/hooks/use-tool-permissions' @@ -20,7 +21,7 @@ type UseWorkflowToolParams = { hasPublishedVersion: boolean hasTriggerNode: boolean inputs?: InputVar[] - outputs?: Variable[] + outputs?: WorkflowToolOutputVariable[] toolPublished?: boolean workflowToolAvailable: boolean onClosePublisher: () => void diff --git a/web/app/components/app/app-publisher/types.ts b/web/app/components/app/app-publisher/types.ts index 8c8e9917cde..bf457fdc1f7 100644 --- a/web/app/components/app/app-publisher/types.ts +++ b/web/app/components/app/app-publisher/types.ts @@ -1,12 +1,20 @@ import type { ModelAndParameter } from '../configuration/debug/types' -import type { InputVar, Variable } from '@/app/components/workflow/types' +import type { WorkflowToolOutputVariable } from '@/app/components/tools/types' +import type { InputVar } from '@/app/components/workflow/types' import type { PublishWorkflowParams } from '@/types/workflow' export type AppPublisherPublishParams = ModelAndParameter | PublishWorkflowParams +export type AppPublisherPublishOptions = { + showSuccessToast?: boolean +} + type AppPublisherPublishHandler = - | ((params?: AppPublisherPublishParams) => Promise | unknown) - | ((params?: unknown) => Promise | unknown) + | (( + params?: AppPublisherPublishParams, + options?: AppPublisherPublishOptions, + ) => Promise | unknown) + | ((params?: unknown, options?: AppPublisherPublishOptions) => Promise | unknown) type AppPublisherRestoreHandler = () => Promise | unknown @@ -25,7 +33,7 @@ export type AppPublisherProps = { crossAxisOffset?: number toolPublished?: boolean inputs?: InputVar[] - outputs?: Variable[] + outputs?: WorkflowToolOutputVariable[] onRefreshData?: () => void workflowToolAvailable?: boolean missingStartNode?: boolean diff --git a/web/app/components/app/app-publisher/workflow-tool-action/index.tsx b/web/app/components/app/app-publisher/workflow-tool-action/index.tsx index aeed4d0bf54..bf9cc3fe444 100644 --- a/web/app/components/app/app-publisher/workflow-tool-action/index.tsx +++ b/web/app/components/app/app-publisher/workflow-tool-action/index.tsx @@ -74,7 +74,10 @@ const WorkflowToolAction = ({
- + {workflowToolLabel} diff --git a/web/app/components/app/deploy/__tests__/index.spec.tsx b/web/app/components/app/deploy/__tests__/index.spec.tsx index 79c1f082294..11888a09817 100644 --- a/web/app/components/app/deploy/__tests__/index.spec.tsx +++ b/web/app/components/app/deploy/__tests__/index.spec.tsx @@ -650,7 +650,7 @@ function render( return renderWithConsoleQuery(ui, { queryClient }) } -let appPermissionKeys: string[] = [AppACLPermission.AccessPoint, AppACLPermission.Deploy] +let appPermissionKeys: string[] = [AppACLPermission.Deploy] let appDetailAvailable = true const mockConsoleState = vi.hoisted(() => ({ workspacePermissionKeys: [] as string[], @@ -744,7 +744,7 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ describe('AppDeploy', () => { beforeEach(() => { vi.clearAllMocks() - appPermissionKeys = [AppACLPermission.AccessPoint, AppACLPermission.Deploy] + appPermissionKeys = [AppACLPermission.Deploy] appDetailAvailable = true mockBuiltInEnvironment.appDetail.enable_api = false mockBuiltInEnvironment.appDetail.enable_site = true @@ -815,18 +815,6 @@ describe('AppDeploy', () => { ).toHaveAttribute('href', '/app/app-1/access-point?environment=canary&accessPoint=serviceApi') }) - it('keeps active access points non-navigable without access point permission', () => { - appPermissionKeys = [AppACLPermission.Deploy] - - render() - - const canaryRow = within(screen.getByRole('row', { name: /Canary/ })) - const webAppLabel = - 'agentV2.agentDetail.access.webApp.title · agentV2.agentDetail.access.status.inService' - expect(canaryRow.queryByRole('link', { name: webAppLabel })).not.toBeInTheDocument() - expect(canaryRow.getByRole('button', { name: webAppLabel })).toBeDisabled() - }) - it('renders the built-in version, access points, and publisher from live app data', () => { render() diff --git a/web/app/components/app/deploy/built-in-environment-card/index.tsx b/web/app/components/app/deploy/built-in-environment-card/index.tsx index 607250e7ce6..d04dce259a8 100644 --- a/web/app/components/app/deploy/built-in-environment-card/index.tsx +++ b/web/app/components/app/deploy/built-in-environment-card/index.tsx @@ -19,7 +19,7 @@ function Divider() { return
} -export function BuiltInEnvironmentCard({ canAccessPoint = false }: { canAccessPoint?: boolean }) { +export function BuiltInEnvironmentCard() { const { t } = useTranslation('deployments') const { formatTime } = useTimestamp() const appDetail = useAppStore((state) => state.appDetail) @@ -90,9 +90,7 @@ export function BuiltInEnvironmentCard({ canAccessPoint = false }: { canAccessPo key={accessPoint} accessPoint={accessPoint} active={activeAccessPoints[accessPoint]} - href={ - canAccessPoint ? getAccessPointHref(appId, 'built-in', accessPoint) : undefined - } + href={getAccessPointHref(appId, 'built-in', accessPoint)} /> ))}
diff --git a/web/app/components/app/deploy/environment-table/index.tsx b/web/app/components/app/deploy/environment-table/index.tsx index cc9b9451e59..cbee27225ed 100644 --- a/web/app/components/app/deploy/environment-table/index.tsx +++ b/web/app/components/app/deploy/environment-table/index.tsx @@ -23,7 +23,6 @@ import { EnvironmentRow } from './row' type EnvironmentTableProps = { appId: string - canAccessPoint?: boolean onChangeVersion?: (deployment: EnvironmentDeployment) => void onDeployLatest?: (deployment: EnvironmentDeployment) => void onDeployToEnvironment?: (environment: AppEnvironment) => void @@ -33,7 +32,6 @@ type EnvironmentTableProps = { export function EnvironmentTable({ appId, - canAccessPoint = false, onChangeVersion, onDeployLatest, onDeployToEnvironment, @@ -134,7 +132,6 @@ export function EnvironmentTable({ void onDeployLatest?: (deployment: EnvironmentDeployment) => void @@ -62,11 +60,7 @@ export function EnvironmentRow({ key={accessPoint} accessPoint={accessPoint} active={isAccessPointActive(accessPoint)} - href={ - canAccessPoint - ? getAccessPointHref(appId, row.environment.id, accessPoint) - : undefined - } + href={getAccessPointHref(appId, row.environment.id, accessPoint)} /> ))}
diff --git a/web/app/components/app/deploy/index.tsx b/web/app/components/app/deploy/index.tsx index 4f1bee0ed1e..c22034584dd 100644 --- a/web/app/components/app/deploy/index.tsx +++ b/web/app/components/app/deploy/index.tsx @@ -22,7 +22,7 @@ import { useRefreshAppEnvironmentsAfterDeploymentPolling } from './use-refresh-a import { useUndeployWorkflow } from './use-undeploy-workflow' import { toDeploymentVersion } from './version' -function AppDeployContent({ appId, canAccessPoint }: { appId: string; canAccessPoint: boolean }) { +function AppDeployContent({ appId }: { appId: string }) { const { t } = useTranslation('deployments') const { t: tCommon } = useTranslation('common') const { t: tWorkflow } = useTranslation('workflow') @@ -86,10 +86,9 @@ function AppDeployContent({ appId, canAccessPoint }: { appId: string; canAccessP
- + setDeploymentRequest({ environment: environment.display_name, @@ -140,17 +139,17 @@ export default function AppDeploy() { if (!appDetail) return - const appACLCapabilities = getAppACLCapabilities(appDetail.permission_keys, { + const canDeploy = getAppACLCapabilities(appDetail.permission_keys, { currentUserId, resourceMaintainer: appDetail.maintainer, workspacePermissionKeys, - }) + }).canDeploy - if (appDetail.mode !== AppModeEnum.WORKFLOW || !appACLCapabilities.canDeploy) return null + if (appDetail.mode !== AppModeEnum.WORKFLOW || !canDeploy) return null return ( - + ) } diff --git a/web/app/components/app/deploy/shared/__tests__/access-point-icon.spec.tsx b/web/app/components/app/deploy/shared/__tests__/access-point-icon.spec.tsx deleted file mode 100644 index 80d18eaaee6..00000000000 --- a/web/app/components/app/deploy/shared/__tests__/access-point-icon.spec.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { screen } from '@testing-library/react' -import { renderWithConsoleQuery as render } from '@/test/console/query-data' -import { AccessPointIcon } from '../access-point-icon' - -describe('AccessPointIcon', () => { - it('links active access points when navigation is allowed', () => { - render( - , - ) - - expect(screen.getByRole('link')).toHaveAttribute( - 'href', - '/app/app-1/access-point?environment=built-in&accessPoint=webApp', - ) - }) - - it('keeps active access points visually active when navigation is not allowed', () => { - render() - - expect(screen.queryByRole('link')).not.toBeInTheDocument() - expect(screen.getByRole('button')).toBeDisabled() - expect(screen.getByRole('button')).not.toHaveClass('opacity-30') - }) - - it('dims inactive access points', () => { - render() - - expect(screen.getByRole('button')).toBeDisabled() - expect(screen.getByRole('button')).toHaveClass('opacity-30') - }) -}) diff --git a/web/app/components/app/deploy/shared/access-point-icon.tsx b/web/app/components/app/deploy/shared/access-point-icon.tsx index 253429f03fc..734e9b694ee 100644 --- a/web/app/components/app/deploy/shared/access-point-icon.tsx +++ b/web/app/components/app/deploy/shared/access-point-icon.tsx @@ -32,7 +32,7 @@ export function AccessPointIcon({ }: { active: boolean accessPoint: AccessPoint - href?: string + href: string }) { const { t } = useTranslation('agentV2') const labels = useAccessPointLabels() @@ -40,11 +40,9 @@ export function AccessPointIcon({ ? t(($) => $['agentDetail.access.status.inService']) : t(($) => $['agentDetail.access.status.outOfService']) const label = `${labels[accessPoint]} · ${status}` - const canNavigate = active && Boolean(href) const triggerClassName = cn( 'flex size-5 shrink-0 items-center justify-center rounded-md border border-divider-regular text-text-secondary shadow-xs outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid', - active && (canNavigate ? 'cursor-pointer hover:bg-state-base-hover' : 'cursor-default'), - !active && 'cursor-not-allowed opacity-30', + active ? 'cursor-pointer hover:bg-state-base-hover' : 'cursor-not-allowed opacity-30', ) const icon = ( @@ -54,7 +52,7 @@ export function AccessPointIcon({ {icon} diff --git a/web/app/components/app/deploy/shared/deployment-status.tsx b/web/app/components/app/deploy/shared/deployment-status.tsx index de7cac3dc53..4f4aabd2076 100644 --- a/web/app/components/app/deploy/shared/deployment-status.tsx +++ b/web/app/components/app/deploy/shared/deployment-status.tsx @@ -68,7 +68,9 @@ export function DeploymentStatus({ status }: { status?: DeploymentStatusValue }) ) : ( )} - {label} + + {label} + ) } diff --git a/web/app/components/app/overview/__tests__/app-chart.spec.tsx b/web/app/components/app/overview/__tests__/app-chart.spec.tsx index aa0e503ff09..07b0b666996 100644 --- a/web/app/components/app/overview/__tests__/app-chart.spec.tsx +++ b/web/app/components/app/overview/__tests__/app-chart.spec.tsx @@ -40,7 +40,7 @@ describe('app-chart', () => { />, ) - expect(screen.getByText('Cost title'))!.toBeInTheDocument() + expect(screen.getByText('Cost title')).toHaveAttribute('title', 'Cost title') expect(screen.getByText('300'))!.toBeInTheDocument() expect(screen.queryByText('Last 7 days'))!.not.toBeInTheDocument() expect(screen.getByText(/\$3\.7500/))!.toBeInTheDocument() diff --git a/web/app/components/app/overview/app-chart.tsx b/web/app/components/app/overview/app-chart.tsx index 7596c7140ec..72f881a71c3 100644 --- a/web/app/components/app/overview/app-chart.tsx +++ b/web/app/components/app/overview/app-chart.tsx @@ -96,7 +96,10 @@ const Chart: React.FC = ({ >
-
+
{title}
{explanation && ( diff --git a/web/app/components/apps/__tests__/app-card.spec.tsx b/web/app/components/apps/__tests__/app-card.spec.tsx index 46a56fb684e..adb020b86b7 100644 --- a/web/app/components/apps/__tests__/app-card.spec.tsx +++ b/web/app/components/apps/__tests__/app-card.spec.tsx @@ -1,5 +1,5 @@ import type { AppPartial } from '@dify/contracts/api/console/apps/types.gen' -import { fireEvent, screen, waitFor } from '@testing-library/react' +import { fireEvent, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry' @@ -1291,6 +1291,21 @@ describe('AppCard', () => { expect(screen.getByText('app.openInExplore')).toBeInTheDocument() }) }) + + it('should hide open in explore for SSO-restricted apps', async () => { + mockWebappAuthEnabled = true + const user = userEvent.setup() + const ssoApp = createMockApp({ access_mode: AccessMode.EXTERNAL_MEMBERS }) + + render() + + await user.click(getOperationsTrigger()) + const menu = await screen.findByRole('menu') + + expect( + within(menu).queryByRole('menuitem', { name: 'app.openInExplore' }), + ).not.toBeInTheDocument() + }) }) describe('Workflow Export with Environment Variables', () => { diff --git a/web/app/components/apps/app-card/interactions.tsx b/web/app/components/apps/app-card/interactions.tsx index 137e04c0bdb..0bdb03c2101 100644 --- a/web/app/components/apps/app-card/interactions.tsx +++ b/web/app/components/apps/app-card/interactions.tsx @@ -55,6 +55,7 @@ import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' +import { AccessMode } from '@/models/access-control' import dynamic from '@/next/dynamic' import { useRouter } from '@/next/navigation' import { useGetUserCanAccessApp } from '@/service/access-control/use-app-access-control' @@ -136,6 +137,7 @@ function AppCardOperationsMenuItems({ const needsPublishBeforeExplore = requiresPublishedWorkflowInExplore(app) && !app.workflow?.id const shouldShowOpenInExploreOption = !app.has_draft_trigger && + app.access_mode !== AccessMode.EXTERNAL_MEMBERS && (needsPublishBeforeExplore || !systemFeatures.webapp_auth.enabled || (!isGettingUserCanAccessApp && Boolean(userCanAccessApp?.result))) diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx index 67343c7834c..1f4194d3918 100644 --- a/web/app/components/base/features/new-feature-panel/index.tsx +++ b/web/app/components/base/features/new-feature-panel/index.tsx @@ -17,6 +17,7 @@ import SpeechToText from '@/app/components/base/features/new-feature-panel/speec import TextToSpeech from '@/app/components/base/features/new-feature-panel/text-to-speech' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { useModalContext } from '@/context/modal-context' type Props = Readonly<{ show: boolean @@ -56,10 +57,11 @@ const NewFeaturePanel = ({ const { t } = useTranslation() const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text) const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts) + const { hasBlockingModalOpen } = useModalContext() return ( ({ diff --git a/web/app/components/datasets/external-api/external-api-modal/index.tsx b/web/app/components/datasets/external-api/external-api-modal/index.tsx index 21567090075..2b1e39a7075 100644 --- a/web/app/components/datasets/external-api/external-api-modal/index.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/index.tsx @@ -246,7 +246,10 @@ const AddExternalAPIModal: FC = ({ >
- + Warning diff --git a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/__tests__/index.spec.tsx index 764b51cb52c..7c4b00da20e 100644 --- a/web/app/components/header/account-setting/access-rules-page/permission-set-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/access-rules-page/permission-set-modal/__tests__/index.spec.tsx @@ -19,7 +19,6 @@ const expectedAppACLPermissionKeys = [ 'app.acl.tracing_config', 'app.acl.log_and_annotation', 'app.acl.access_config', - 'app.acl.access_point_manage', ] const getPermissionKeyMatcher = (permissionKey: string) => diff --git a/web/app/components/integrations/tool-provider-card.tsx b/web/app/components/integrations/tool-provider-card.tsx index f6aca486a20..27dc904c968 100644 --- a/web/app/components/integrations/tool-provider-card.tsx +++ b/web/app/components/integrations/tool-provider-card.tsx @@ -131,13 +131,13 @@ function IntegrationsToolProviderCard({
{!!org && ( <> -
+
{org}
/
)} -
+
{name}
diff --git a/web/app/components/plugins/card/base/org-info.tsx b/web/app/components/plugins/card/base/org-info.tsx index 2d9ef6ce036..9989cd454cb 100644 --- a/web/app/components/plugins/card/base/org-info.tsx +++ b/web/app/components/plugins/card/base/org-info.tsx @@ -13,7 +13,7 @@ const OrgInfo = ({ className, orgName, packageName, packageNameClassName }: Prop {orgName && ( <> {orgName} @@ -23,7 +23,7 @@ const OrgInfo = ({ className, orgName, packageName, packageNameClassName }: Prop )} = ({ } return ( <> -
-

+

+

{isFailed && errMsg ? ( errMsg ) : categoryTarget ? ( @@ -125,7 +130,7 @@ const Installed: FC = ({ )}

{/* Action Buttons */} -
+
{categoryTarget ? ( = ({ @@ -201,7 +201,9 @@ const InstallFromGitHub: React.FC = ({
-
{getTitle()}
+ + {getTitle()} +
{![ InstallStepFromGitHub.uploadFailed, diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/__tests__/index.spec.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/__tests__/index.spec.tsx index 7fb5d9c3239..ababf3dfb69 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/__tests__/index.spec.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/__tests__/index.spec.tsx @@ -419,6 +419,9 @@ describe('InstallFromLocalPackage', () => { await waitFor(() => { expect(screen.getByTestId('ready-to-install-package')).toBeInTheDocument() expect(screen.getByTestId('package-step')).toHaveTextContent('uploadFailed') + expect( + screen.getByRole('dialog', { name: 'plugin.installModal.uploadFailed' }), + ).toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx index f669cc7bfa9..825484b0d4b 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { Dependency, PluginCategoryEnum, PluginDeclaration } from '../../types' import { cn } from '@langgenius/dify-ui/cn' -import { Dialog, DialogClose, DialogContent } from '@langgenius/dify-ui/dialog' +import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' import * as React from 'react' import { useCallback, useState } from 'react' @@ -90,10 +90,10 @@ const InstallFromLocalPackage: React.FC = ({ @@ -109,8 +109,10 @@ const InstallFromLocalPackage: React.FC = ({ } /> -
-
{getTitle()}
+
+ + {getTitle()} +
{step === InstallStep.uploading && ( = ({ -
+
{getTitle()} diff --git a/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx b/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx index 36354d807ed..edc6ccfd410 100644 --- a/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx @@ -184,8 +184,7 @@ describe('Item Component', () => { ) const enterRenameMode = () => { - const firstButton = result.container.querySelectorAll('button')[0] as HTMLElement - fireEvent.click(firstButton) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.rename' })) } return { ...result, onRename, enterRenameMode } @@ -196,7 +195,7 @@ describe('Item Component', () => { enterRenameMode() - expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('textbox', { name: 'common.operation.rename' })).toBeInTheDocument() }) it('should show save and cancel buttons in rename mode', () => { @@ -213,7 +212,7 @@ describe('Item Component', () => { enterRenameMode() - const input = screen.getByRole('textbox') + const input = screen.getByRole('textbox', { name: 'common.operation.rename' }) fireEvent.change(input, { target: { value: 'New Name' } }) fireEvent.click(screen.getByText('common.operation.save')) @@ -228,7 +227,7 @@ describe('Item Component', () => { const { enterRenameMode } = renderWithRenameEnabled() enterRenameMode() - expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('textbox', { name: 'common.operation.rename' })).toBeInTheDocument() fireEvent.click(screen.getByText('common.operation.cancel')) @@ -240,7 +239,7 @@ describe('Item Component', () => { enterRenameMode() - const input = screen.getByRole('textbox') + const input = screen.getByRole('textbox', { name: 'common.operation.rename' }) fireEvent.change(input, { target: { value: 'Updated Value' } }) expect(input).toHaveValue('Updated Value') diff --git a/web/app/components/plugins/plugin-auth/authorized/item.tsx b/web/app/components/plugins/plugin-auth/authorized/item.tsx index 76da4b84066..7bef9a46149 100644 --- a/web/app/components/plugins/plugin-auth/authorized/item.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/item.tsx @@ -2,6 +2,7 @@ import type { Credential } from '../types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' +import { Input } from '@langgenius/dify-ui/input' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiInformationLine } from '@remixicon/react' @@ -9,7 +10,6 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { memo, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' -import Input from '@/app/components/base/input' import { userProfileQueryOptions } from '@/features/account-profile/client' import { useCredentialPermissions } from '@/hooks/use-credential-permissions' import { CredentialTypeEnum } from '../types' @@ -85,10 +85,10 @@ const Item = ({ {renaming && (
$['operation.rename'], { ns: 'common' })} + className="h-6 grow" value={renameValue} - onChange={(e) => setRenameValue(e.target.value)} + onValueChange={setRenameValue} placeholder={t(($) => $['placeholder.input'], { ns: 'common' })} onClick={(e) => e.stopPropagation()} /> diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index 22f5a212cab..094898ea58e 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -4,7 +4,6 @@ import type { PluginDetail } from '@/app/components/plugins/types' import { cn } from '@langgenius/dify-ui/cn' import { Drawer, - DrawerBackdrop, DrawerContent, DrawerPopup, DrawerPortal, @@ -68,18 +67,18 @@ const PluginDetailPanel: FC = ({ return ( { if (!open) onHide() }} > - - + diff --git a/web/app/components/plugins/plugin-item/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-item/__tests__/index.spec.tsx index 456028c6ec2..b988542d401 100644 --- a/web/app/components/plugins/plugin-item/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-item/__tests__/index.spec.tsx @@ -614,7 +614,9 @@ describe('PluginItem', () => { // Assert const pluginContainer = container.firstChild as HTMLElement - expect(pluginContainer).toHaveClass('border-components-option-card-option-selected-border') + expect(pluginContainer).toHaveClass( + 'after:inset-ring-components-option-card-option-selected-border', + ) }) it('should not highlight unselected plugin', () => { @@ -628,7 +630,7 @@ describe('PluginItem', () => { // Assert const pluginContainer = container.firstChild as HTMLElement expect(pluginContainer).not.toHaveClass( - 'border-components-option-card-option-selected-border', + 'after:inset-ring-components-option-card-option-selected-border', ) }) @@ -654,9 +656,18 @@ describe('PluginItem', () => { // Assert expect(screen.getByTestId('plugin-action').parentElement).toHaveClass( + 'absolute', + 'top-1/2', + 'right-0', + '-translate-y-1/2', + 'pointer-events-none', 'opacity-0', + 'group-hover/plugin-item:pointer-events-auto', 'group-hover/plugin-item:opacity-100', - 'focus-within:opacity-100', + 'group-focus-within/plugin-item:pointer-events-auto', + 'group-focus-within/plugin-item:opacity-100', + '[@media(hover:none)]:pointer-events-auto', + '[@media(hover:none)]:opacity-100', ) }) }) diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index edd886574d0..db723f6cfed 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -120,8 +120,9 @@ const PluginItem: FC = ({ return (
= ({ >
@@ -188,10 +189,14 @@ const PluginItem: FC = ({ } />
-
- +
+
e.stopPropagation()} > = ({
-
+
{/* Organization & Name */}
{ >
- + {confirmTitle} diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index a1e5ef098a1..210dd6c8770 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -2,7 +2,7 @@ import type { DatasourceProviderType, ToolProviderType, } from '@dify/contracts/api/console/workspaces/types.gen' -import type { VarType } from '../workflow/types' +import type { Variable, VarType } from '../workflow/types' type LocalizedText = { en_US: T @@ -212,10 +212,21 @@ export type WorkflowToolProviderParameter = { type?: string } +export type WorkflowToolOutputSource = { + nodeId: string + nodeTitle: string + outputIndex: number +} + +export type WorkflowToolOutputVariable = Variable & { + source?: WorkflowToolOutputSource +} + export type WorkflowToolProviderOutputParameter = { name: string description: string type?: VarType + source?: WorkflowToolOutputSource reserved?: boolean } diff --git a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx index 5ce7f7bfff9..96d372a0b21 100644 --- a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import type { WorkflowToolDrawerPayload } from '../index' -import { render, screen, waitFor } from '@testing-library/react' +import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { WorkflowToolDrawer } from '../index' @@ -112,6 +112,7 @@ describe('WorkflowToolDrawer', () => { labels: ['label1', 'new-label'], }), ) + expect(onCreate.mock.calls[0]![0]).not.toHaveProperty('outputParameters') }) it('should block invalid tool-call names before saving', async () => { @@ -158,11 +159,165 @@ describe('WorkflowToolDrawer', () => { }) }) - it('should show duplicate reserved output warnings', () => { + it('should not show output warnings when names are valid and unique', () => { + render( + , + ) + + expect( + screen.queryByRole('button', { + name: /reservedParameterDuplicateTip|duplicateOutputVariable/, + }), + ).not.toBeInTheDocument() + }) + + it('should show one reserved-name warning on the user output only', () => { render( , ) - expect(screen.getAllByTestId('reserved-output-warning').length).toBeGreaterThan(0) + const userOutputRow = screen.getByRole('row', { name: /text.*Reserved output duplicate/ }) + expect( + within(userOutputRow).getByRole('button', { + name: 'tools.createTool.toolOutput.reservedParameterDuplicateTip', + }), + ).toBeInTheDocument() + + const reservedOutputRow = screen.getByRole('row', { + name: /text.*tools\.createTool\.toolOutput\.reserved.*string/, + }) + expect(within(reservedOutputRow).queryByRole('button')).not.toBeInTheDocument() + }) + + it('should identify the End node sources for duplicate outputs', async () => { + const user = userEvent.setup() + const outputParameters: WorkflowToolDrawerPayload['outputParameters'] = [ + { + name: 'result', + description: 'Success output', + source: { nodeId: 'end-success', nodeTitle: 'Success End', outputIndex: 0 }, + }, + { + name: 'result', + description: 'Fallback output', + source: { nodeId: 'end-fallback', nodeTitle: 'Fallback End', outputIndex: 0 }, + }, + ] + + render( + , + ) + + expect( + screen.getByRole('row', { name: /result.*Success End.*Success output/ }), + ).toBeInTheDocument() + expect( + screen.getByRole('row', { name: /result.*Fallback End.*Fallback output/ }), + ).toBeInTheDocument() + + const duplicateWarnings = screen.getAllByRole('button', { + name: /workflow\.errorMsg\.duplicateOutputVariable/, + }) + expect(duplicateWarnings).toHaveLength(2) + + await user.click(duplicateWarnings[0]!) + + const duplicateDetails = (await screen.findAllByRole('dialog')).find((dialog) => + within(dialog).queryByText(/workflow\.errorMsg\.duplicateOutputVariable/), + )! + expect(duplicateDetails).toHaveTextContent('Success End') + expect(duplicateDetails).toHaveTextContent('Fallback End') + }) + + it('should combine reserved-name and duplicate-name issues into one warning per user output', async () => { + const user = userEvent.setup() + const outputParameters: WorkflowToolDrawerPayload['outputParameters'] = [ + { + name: 'text', + description: 'Success output', + source: { nodeId: 'end-success', nodeTitle: 'Output', outputIndex: 0 }, + }, + { + name: 'text', + description: 'Fallback output', + source: { nodeId: 'end-fallback', nodeTitle: 'Output', outputIndex: 0 }, + }, + ] + + render( + , + ) + + const issueWarnings = screen.getAllByRole('button', { + name: /tools\.createTool\.toolOutput\.reservedParameterDuplicateTip.*workflow\.errorMsg\.duplicateOutputVariable/, + }) + expect(issueWarnings).toHaveLength(2) + + await user.click(issueWarnings[0]!) + + const issueDetails = (await screen.findAllByRole('dialog')).find((dialog) => + within(dialog).queryByText('tools.createTool.toolOutput.reservedParameterDuplicateTip'), + )! + expect(issueDetails).toHaveTextContent( + 'tools.createTool.toolOutput.reservedParameterDuplicateTip', + ) + expect(issueDetails).toHaveTextContent('workflow.errorMsg.duplicateOutputVariable') + expect(issueDetails).toHaveTextContent('Output (1/2)') + expect(issueDetails).toHaveTextContent('Output (2/2)') + expect(issueDetails).not.toHaveTextContent('end-succ') + expect(issueDetails).not.toHaveTextContent('end-fall') + + expect(screen.getByRole('row', { name: /Output \(1\/2\).*Success output/ })).toBeInTheDocument() + expect( + screen.getByRole('row', { name: /Output \(2\/2\).*Fallback output/ }), + ).toBeInTheDocument() + + const reservedOutputRow = screen.getByRole('row', { + name: /text.*tools\.createTool\.toolOutput\.reserved.*string/, + }) + expect(within(reservedOutputRow).queryByRole('button')).not.toBeInTheDocument() + }) + + it('should keep schema-derived outputs compact when source metadata is unavailable', () => { + render( + , + ) + + expect(screen.getByRole('row', { name: /answer string Published answer/ })).toBeInTheDocument() + expect(screen.queryByText('tools.createTool.toolOutput.sourceNode')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/tools/workflow-tool/__tests__/utils.test.ts b/web/app/components/tools/workflow-tool/__tests__/utils.test.ts index 4fe0f1917e4..c6593c16202 100644 --- a/web/app/components/tools/workflow-tool/__tests__/utils.test.ts +++ b/web/app/components/tools/workflow-tool/__tests__/utils.test.ts @@ -1,9 +1,67 @@ import type { + WorkflowToolOutputSource, WorkflowToolProviderOutputParameter, WorkflowToolProviderOutputSchema, } from '../../types' import { VarType } from '@/app/components/workflow/types' -import { buildWorkflowOutputParameters } from '../utils' +import { + buildWorkflowOutputParameters, + getDuplicateWorkflowOutputGroups, + getSourceNodeDisplayName, + getUniqueWorkflowOutputSources, +} from '../utils' + +describe('workflow output sources', () => { + const duplicateSources: WorkflowToolOutputSource[] = [ + { nodeId: 'end-success', nodeTitle: 'Output', outputIndex: 0 }, + { nodeId: 'end-fallback', nodeTitle: 'Output', outputIndex: 0 }, + ] + + it('deduplicates source nodes while preserving their order', () => { + const outputs: WorkflowToolProviderOutputParameter[] = [ + { name: 'result', description: '', source: duplicateSources[0] }, + { name: 'result', description: '', source: duplicateSources[0] }, + { name: 'result', description: '', source: duplicateSources[1] }, + ] + + expect(getUniqueWorkflowOutputSources(outputs)).toEqual(duplicateSources) + }) + + it('numbers sources with the same title without exposing their node IDs', () => { + expect(getSourceNodeDisplayName(duplicateSources[0]!, duplicateSources)).toBe('Output (1/2)') + expect(getSourceNodeDisplayName(duplicateSources[1]!, duplicateSources)).toBe('Output (2/2)') + }) +}) + +describe('getDuplicateWorkflowOutputGroups', () => { + it('groups trimmed duplicate names while preserving their End node sources', () => { + const params: WorkflowToolProviderOutputParameter[] = [ + { + name: ' result ', + description: 'Success output', + source: { nodeId: 'end-success', nodeTitle: 'Success End', outputIndex: 0 }, + }, + { + name: 'result', + description: 'Fallback output', + source: { nodeId: 'end-fallback', nodeTitle: 'Fallback End', outputIndex: 0 }, + }, + { + name: 'unique', + description: 'Unique output', + source: { nodeId: 'end-unique', nodeTitle: 'Unique End', outputIndex: 0 }, + }, + ] + + const result = getDuplicateWorkflowOutputGroups(params) + + expect([...result.keys()]).toEqual(['result']) + expect(result.get('result')?.map((item) => item.source?.nodeTitle)).toEqual([ + 'Success End', + 'Fallback End', + ]) + }) +}) describe('buildWorkflowOutputParameters', () => { it('returns provided output parameters when array input exists', () => { @@ -13,7 +71,7 @@ describe('buildWorkflowOutputParameters', () => { const result = buildWorkflowOutputParameters(params, null) - expect(result).toBe(params) + expect(result).toEqual(params) }) it('fills missing output description and type from schema when array input exists', () => { diff --git a/web/app/components/tools/workflow-tool/helpers.ts b/web/app/components/tools/workflow-tool/helpers.ts index 7ecc8f689d0..02057150686 100644 --- a/web/app/components/tools/workflow-tool/helpers.ts +++ b/web/app/components/tools/workflow-tool/helpers.ts @@ -56,7 +56,7 @@ export const hasReservedWorkflowOutputConflict = ( } export const getWorkflowOutputParameters = ( - rawOutputParameters: WorkflowToolProviderOutputParameter[], + rawOutputParameters: WorkflowToolProviderOutputParameter[] | undefined, outputSchema?: WorkflowToolProviderOutputSchema, ) => { return buildWorkflowOutputParameters(rawOutputParameters, outputSchema) diff --git a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts index 0f8720f10be..d5d70c448f4 100644 --- a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts +++ b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts @@ -1,8 +1,9 @@ import type { + WorkflowToolOutputVariable, WorkflowToolProviderRequest, WorkflowToolProviderResponse, } from '@/app/components/tools/types' -import type { InputVar, Variable } from '@/app/components/workflow/types' +import type { InputVar } from '@/app/components/workflow/types' import { act, renderHook } from '@testing-library/react' import { InputVarType } from '@/app/components/workflow/types' import { isParametersOutdated, useConfigureButton } from '../use-configure-button' @@ -46,12 +47,15 @@ const createMockInputVar = (overrides: Partial = {}): InputVar => ...overrides, }) as InputVar -const createMockVariable = (overrides: Partial = {}): Variable => +const createMockVariable = ( + overrides: Partial = {}, +): WorkflowToolOutputVariable => ({ variable: 'output_var', value_type: 'string', + source: { nodeId: 'end-1', nodeTitle: 'Success End', outputIndex: 0 }, ...overrides, - }) as Variable + }) as WorkflowToolOutputVariable const createMockDetail = ( overrides: Partial = {}, @@ -293,6 +297,10 @@ describe('useConfigureButton', () => { form: 'llm', description: '', }) + expect(result.current.payload.outputParameters[0]).toMatchObject({ + name: 'output_var', + source: { nodeId: 'end-1', nodeTitle: 'Success End', outputIndex: 0 }, + }) }) it('should use detail values when published with detail', () => { @@ -370,6 +378,46 @@ describe('useConfigureButton', () => { // Mutation handlers describe('handleCreate', () => { + it('should publish before creating the provider', async () => { + mockCreateWorkflowToolProvider.mockResolvedValue({}) + const handlePublish = vi.fn().mockResolvedValue(undefined) + const { result } = renderHook(() => + useConfigureButton(createDefaultOptions({ handlePublish })), + ) + + await act(async () => { + await result.current.handleCreate( + createMockRequest({ workflow_app_id: 'app-123' }) as WorkflowToolProviderRequest & { + workflow_app_id: string + }, + ) + }) + + expect(handlePublish).toHaveBeenCalledOnce() + expect(mockCreateWorkflowToolProvider).toHaveBeenCalledOnce() + expect(handlePublish.mock.invocationCallOrder[0]).toBeLessThan( + mockCreateWorkflowToolProvider.mock.invocationCallOrder[0]!, + ) + }) + + it('should not create the provider when publishing fails', async () => { + const handlePublish = vi.fn().mockRejectedValue(new Error('Publish failed')) + const { result } = renderHook(() => + useConfigureButton(createDefaultOptions({ handlePublish })), + ) + + await act(async () => { + await result.current.handleCreate( + createMockRequest({ workflow_app_id: 'app-123' }) as WorkflowToolProviderRequest & { + workflow_app_id: string + }, + ) + }) + + expect(mockCreateWorkflowToolProvider).not.toHaveBeenCalled() + expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: 'Publish failed' }) + }) + it('should create provider, invalidate caches, refresh, and notify configured', async () => { mockCreateWorkflowToolProvider.mockResolvedValue({}) const onRefreshData = vi.fn() @@ -439,6 +487,7 @@ describe('useConfigureButton', () => { expect(onRefreshData).toHaveBeenCalled() expect(mockInvalidateAllWorkflowTools).toHaveBeenCalled() expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123') + expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: expect.any(String) }) expect(onConfigured).toHaveBeenCalled() }) diff --git a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts index 6c8e982c624..3fa0e39e3e4 100644 --- a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts +++ b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts @@ -1,11 +1,12 @@ import type { Emoji, + WorkflowToolOutputVariable, WorkflowToolProviderOutputParameter, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse, } from '@/app/components/tools/types' -import type { InputVar, Variable } from '@/app/components/workflow/types' +import type { InputVar } from '@/app/components/workflow/types' import type { PublishWorkflowParams } from '@/types/workflow' import { toast } from '@langgenius/dify-ui/toast' import { useEffect, useMemo, useRef } from 'react' @@ -67,16 +68,19 @@ function buildExistingParameters( }) } -function buildNewOutputParameters(outputs?: Variable[]): WorkflowToolProviderOutputParameter[] { +function buildNewOutputParameters( + outputs?: WorkflowToolOutputVariable[], +): WorkflowToolProviderOutputParameter[] { return (outputs || []).map((item) => ({ name: item.variable, description: '', type: item.value_type, + source: item.source, })) } function buildExistingOutputParameters( - outputs: Variable[] | undefined, + outputs: WorkflowToolOutputVariable[] | undefined, detail: WorkflowToolProviderResponse, ): WorkflowToolProviderOutputParameter[] { return (outputs || []).map((item) => { @@ -85,6 +89,7 @@ function buildExistingOutputParameters( name: item.variable, description: found ? found.description : '', type: item.value_type, + source: item.source, } }) } @@ -100,7 +105,7 @@ type UseConfigureButtonOptions = { name: string description: string inputs?: InputVar[] - outputs?: Variable[] + outputs?: WorkflowToolOutputVariable[] handlePublish: (params?: PublishWorkflowParams) => Promise onRefreshData?: () => void onConfigured?: () => void @@ -121,7 +126,6 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { onRefreshData, onConfigured, } = options - const { t } = useTranslation() // Data fetching via React Query @@ -180,6 +184,7 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { // Mutation handlers (not memoized — only used in conditionally-rendered modal) const handleCreate = async (data: WorkflowToolProviderRequest & { workflow_app_id: string }) => { try { + await handlePublish() await createWorkflowToolProvider(data) invalidateAllWorkflowTools() onRefreshData?.() @@ -204,6 +209,7 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { onRefreshData?.() invalidateAllWorkflowTools() invalidateDetail(workflowAppId) + toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) onConfigured?.() } catch (e) { toast.error((e as Error).message) diff --git a/web/app/components/tools/workflow-tool/index.stories.tsx b/web/app/components/tools/workflow-tool/index.stories.tsx new file mode 100644 index 00000000000..bf418e253ac --- /dev/null +++ b/web/app/components/tools/workflow-tool/index.stories.tsx @@ -0,0 +1,132 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import type { WorkflowToolDrawerPayload } from '.' +import { fn } from 'storybook/test' +import { VarType } from '@/app/components/workflow/types' +import { WorkflowToolDrawer } from '.' + +const payload = { + icon: { + content: '🔀', + background: '#EEF4FF', + }, + label: 'Branching Workflow', + name: 'branching_workflow', + description: 'Returns different outputs depending on the executed branch.', + parameters: [ + { + name: 'route', + description: 'Selects the branch to execute.', + form: 'llm', + required: true, + type: 'string', + }, + ], + outputParameters: [ + { + name: 'aaa', + description: 'Output from the first End node.', + type: VarType.string, + source: { nodeId: 'end-success', nodeTitle: 'Success End', outputIndex: 0 }, + }, + { + name: 'bbb', + description: 'Output from the second End node.', + type: VarType.number, + source: { nodeId: 'end-fallback', nodeTitle: 'Fallback End', outputIndex: 0 }, + }, + ], + labels: [], + privacy_policy: '', + workflow_app_id: 'workflow-app-story', +} satisfies WorkflowToolDrawerPayload + +const meta = { + title: 'Tools/WorkflowTool/Drawer', + component: WorkflowToolDrawer, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Configures a published workflow as a tool. This story covers the union of outputs from multiple End nodes.', + }, + }, + }, + args: { + isAdd: true, + payload, + onCreate: fn(), + onHide: fn(), + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const MultipleEndOutputs: Story = {} + +export const PublishedOutputsWithoutDraftSources: Story = { + args: { + isAdd: false, + payload: { + ...payload, + outputParameters: undefined, + tool: { + output_schema: { + type: 'object', + properties: { + answer: { + type: VarType.string, + description: 'Output loaded from the published tool schema.', + }, + }, + }, + }, + workflow_tool_id: 'workflow-tool-story', + }, + }, +} + +export const DuplicateEndOutputs: Story = { + args: { + payload: { + ...payload, + outputParameters: [ + { + name: 'result', + description: 'Output from the successful branch.', + type: VarType.string, + source: { nodeId: 'end-success', nodeTitle: 'Success End', outputIndex: 0 }, + }, + { + name: 'result', + description: 'Output from the fallback branch.', + type: VarType.number, + source: { nodeId: 'end-fallback', nodeTitle: 'Fallback End', outputIndex: 0 }, + }, + ], + }, + }, +} + +export const DuplicateReservedEndOutputs: Story = { + args: { + payload: { + ...payload, + outputParameters: [ + { + name: 'text', + description: 'Output from the successful branch.', + type: VarType.string, + source: { nodeId: 'end-success', nodeTitle: 'Output', outputIndex: 0 }, + }, + { + name: 'text', + description: 'Output from the fallback branch.', + type: VarType.number, + source: { nodeId: 'end-fallback', nodeTitle: 'Output', outputIndex: 0 }, + }, + ], + }, + }, +} diff --git a/web/app/components/tools/workflow-tool/index.tsx b/web/app/components/tools/workflow-tool/index.tsx index c2da9f97a29..3272dcf28c3 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -2,6 +2,7 @@ import type { DrawerProps } from '@langgenius/dify-ui/drawer' import type { Emoji, + WorkflowToolOutputSource, WorkflowToolProviderOutputParameter, WorkflowToolProviderOutputSchema, WorkflowToolProviderParameter, @@ -21,7 +22,6 @@ import { } from '@langgenius/dify-ui/drawer' import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { produce } from 'immer' import * as React from 'react' import { useMemo, useState } from 'react' @@ -33,6 +33,7 @@ import Input from '@/app/components/base/input' import LabelSelector from '@/app/components/tools/labels/selector' import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal' import MethodSelector from '@/app/components/tools/workflow-tool/method-selector' +import { normalizeWorkflowOutputName } from '@/app/components/workflow/utils/variable' import { buildWorkflowToolRequestPayload, getReservedWorkflowOutputParameters, @@ -40,6 +41,11 @@ import { hasReservedWorkflowOutputConflict, isWorkflowToolNameValid, } from './helpers' +import { + getDuplicateWorkflowOutputGroups, + getSourceNodeDisplayName, + getUniqueWorkflowOutputSources, +} from './utils' export type WorkflowToolDrawerPayload = { icon: Emoji @@ -47,7 +53,7 @@ export type WorkflowToolDrawerPayload = { name: string description: string parameters: WorkflowToolProviderParameter[] - outputParameters: WorkflowToolProviderOutputParameter[] + outputParameters?: WorkflowToolProviderOutputParameter[] labels: string[] privacy_policy: string tool?: { @@ -135,6 +141,88 @@ const WorkflowToolDrawerFrame = ({ ) } +const WorkflowToolOutputName = React.memo( + ({ + duplicateSources, + item, + reservedOutputParameters, + }: { + duplicateSources?: WorkflowToolOutputSource[] + item: WorkflowToolProviderOutputParameter + reservedOutputParameters: WorkflowToolProviderOutputParameter[] + }) => { + const { t } = useTranslation() + const reservedOutputDuplicateTip = t( + ($) => $['createTool.toolOutput.reservedParameterDuplicateTip'], + { ns: 'tools' }, + ) + const sourceNodeLabel = t(($) => $['createTool.toolOutput.sourceNode'], { ns: 'tools' }) + const duplicateOutputTip = t(($) => $['errorMsg.duplicateOutputVariable'], { + ns: 'workflow', + variable: normalizeWorkflowOutputName(item.name), + }) + const hasReservedNameConflict = + !item.reserved && hasReservedWorkflowOutputConflict(reservedOutputParameters, item.name) + const hasDuplicateNameConflict = !item.reserved && !!duplicateSources + const issueLabel = hasReservedNameConflict + ? hasDuplicateNameConflict + ? `${reservedOutputDuplicateTip} ${duplicateOutputTip}` + : reservedOutputDuplicateTip + : duplicateOutputTip + const sources = duplicateSources || [] + + return ( +
+
+ {item.name} + {item.reserved && ( + + {t(($) => $['createTool.toolOutput.reserved'], { ns: 'tools' })} + + )} + {hasReservedNameConflict || hasDuplicateNameConflict ? ( + +
+ {hasReservedNameConflict ?

{reservedOutputDuplicateTip}

: null} + {hasDuplicateNameConflict ? ( +
+

{duplicateOutputTip}

+ {sources.length > 0 ? ( +
    + {sources.map((source) => { + const sourceTitle = getSourceNodeDisplayName(source, sources) + return ( +
  • + {sourceNodeLabel}: {sourceTitle} +
  • + ) + })} +
+ ) : null} +
+ ) : null} +
+
+ ) : null} +
+
{item.type}
+ {hasDuplicateNameConflict && item.source ? ( +
+ {sourceNodeLabel} ·{' '} + {getSourceNodeDisplayName(item.source, sources)} +
+ ) : null} +
+ ) + }, +) + export function WorkflowToolDrawer({ isAdd, payload, @@ -158,6 +246,15 @@ export function WorkflowToolDrawer({ [rawOutputParameters, outputSchema], ) const reservedOutputParameters = useMemo(() => getReservedWorkflowOutputParameters(t), [t]) + const duplicateOutputSourceGroups = useMemo(() => { + const groups = getDuplicateWorkflowOutputGroups(outputParameters) + const sourceGroups = new Map() + + for (const [name, outputs] of groups) + sourceGroups.set(name, getUniqueWorkflowOutputSources(outputs)) + + return sourceGroups + }, [outputParameters]) const handleParameterChange = (key: string, value: string, index: number) => { const newData = produce(parameters, (draft: WorkflowToolProviderParameter[]) => { @@ -392,46 +489,24 @@ export function WorkflowToolDrawer({ {[...reservedOutputParameters, ...outputParameters].map((item, index) => ( - + -
-
- - {item.name} - - - {item.reserved - ? t(($) => $['createTool.toolOutput.reserved'], { ns: 'tools' }) - : ''} - - {!item.reserved && - hasReservedWorkflowOutputConflict( - reservedOutputParameters, - item.name, - ) ? ( - - - } - /> - -
- {t( - ($) => - $['createTool.toolOutput.reservedParameterDuplicateTip'], - { ns: 'tools' }, - )} -
-
-
- ) : null} -
-
{item.type}
-
+ diff --git a/web/app/components/tools/workflow-tool/utils.ts b/web/app/components/tools/workflow-tool/utils.ts index 5273535bab5..a4a1da8dd69 100644 --- a/web/app/components/tools/workflow-tool/utils.ts +++ b/web/app/components/tools/workflow-tool/utils.ts @@ -1,8 +1,10 @@ import type { + WorkflowToolOutputSource, WorkflowToolProviderOutputParameter, WorkflowToolProviderOutputSchema, } from '../types' import { VarType } from '@/app/components/workflow/types' +import { normalizeWorkflowOutputName } from '@/app/components/workflow/utils/variable' const validVarTypes = new Set(Object.values(VarType)) @@ -12,6 +14,62 @@ const normalizeVarType = (type?: string): VarType | undefined => { return validVarTypes.has(type) ? (type as VarType) : undefined } +export const getUniqueWorkflowOutputSources = ( + outputParameters: readonly WorkflowToolProviderOutputParameter[], +): WorkflowToolOutputSource[] => { + const sources: WorkflowToolOutputSource[] = [] + const sourceNodeIds = new Set() + + for (const output of outputParameters) { + const source = output.source + if (!source || sourceNodeIds.has(source.nodeId)) continue + + sourceNodeIds.add(source.nodeId) + sources.push(source) + } + + return sources +} + +export const getSourceNodeDisplayName = ( + source: WorkflowToolOutputSource, + duplicateSources: readonly WorkflowToolOutputSource[], +) => { + const title = source.nodeTitle || source.nodeId + if (!source.nodeTitle) return title + + let sameTitleSourceCount = 0 + let sourceIndex = -1 + + for (const candidate of duplicateSources) { + if (candidate.nodeTitle !== source.nodeTitle) continue + + if (candidate.nodeId === source.nodeId) sourceIndex = sameTitleSourceCount + sameTitleSourceCount += 1 + } + + if (sameTitleSourceCount < 2 || sourceIndex < 0) return title + + return `${title} (${sourceIndex + 1}/${sameTitleSourceCount})` +} + +export const getDuplicateWorkflowOutputGroups = ( + outputParameters: WorkflowToolProviderOutputParameter[], +) => { + const groups = new Map() + + for (const item of outputParameters) { + const name = normalizeWorkflowOutputName(item.name) + if (!name) continue + + const group = groups.get(name) || [] + group.push(item) + groups.set(name, group) + } + + return new Map([...groups].filter(([, items]) => items.length > 1)) +} + export const buildWorkflowOutputParameters = ( outputParameters: WorkflowToolProviderOutputParameter[] | null | undefined, outputSchema?: WorkflowToolProviderOutputSchema | null, diff --git a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx index cafae7b2526..54ddf212a8f 100644 --- a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx @@ -149,6 +149,7 @@ vi.mock('@/app/components/app/app-publisher', () => ({ data-start-node-limit-exceeded={String(Boolean(props.startNodeLimitExceeded))} data-has-trigger-node={String(Boolean(props.hasTriggerNode))} data-inputs={JSON.stringify(inputs)} + data-outputs={JSON.stringify(props.outputs ?? [])} > + + + + ) +} + const createFormInput = (overrides: Partial = {}): FormInputItem => ({ type: InputVarType.paragraph, output_variable_name: 'user_name', @@ -251,6 +272,70 @@ describe('human-input/delivery-method/test-email-sender', () => { expect(handleOpenChange).toHaveBeenCalledWith(false) }) + it('should start a fresh session after closing and reopening', async () => { + const user = userEvent.setup() + const { requests } = setupFetch() + renderWithProviders() + + await user.click( + screen.getByRole('button', { + name: 'workflow.nodes.humanInput.deliveryMethod.emailSender.send', + }), + ) + expect( + await screen.findByText('workflow.nodes.humanInput.deliveryMethod.emailSender.done'), + ).toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'common.operation.ok' })) + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + await user.click(screen.getByRole('button', { name: 'Open test email sender' })) + + expect( + screen.getByRole('button', { + name: 'workflow.nodes.humanInput.deliveryMethod.emailSender.send', + }), + ).toBeInTheDocument() + expect( + screen.queryByText('workflow.nodes.humanInput.deliveryMethod.emailSender.done'), + ).not.toBeInTheDocument() + + await user.click( + screen.getByRole('button', { + name: 'workflow.nodes.humanInput.deliveryMethod.emailSender.send', + }), + ) + await waitFor(() => { + expect( + requests.filter( + (request) => request.method === 'POST' && request.url.endsWith('/delivery-test'), + ), + ).toHaveLength(2) + }) + }) + + it('should stay open when clicking outside the dialog', async () => { + const user = userEvent.setup() + const handleOpenChange = vi.fn() + + renderWithProviders( + , + ) + + await user.click(document.body) + + expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(handleOpenChange).not.toHaveBeenCalled() + }) + it('should submit variables referenced by dynamic select option sources', async () => { const user = userEvent.setup() const { requests } = setupFetch() diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx index e9f2067d338..2a4f8614eec 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx @@ -3,13 +3,13 @@ import type { Node, NodeOutPutVar } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { IconButton } from '@langgenius/dify-ui/icon-button' +import { Input } from '@langgenius/dify-ui/input' import { Switch } from '@langgenius/dify-ui/switch' import { toast } from '@langgenius/dify-ui/toast' import { RiBugLine } from '@remixicon/react' import { useSuspenseQuery } from '@tanstack/react-query' -import { memo, useCallback, useState } from 'react' +import { memo, useCallback, useId, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' -import Input from '@/app/components/base/input' import { userProfileQueryOptions } from '@/features/account-profile/client' import MailBodyInput from './mail-body-input' import Recipient from './recipient' @@ -34,6 +34,7 @@ const EmailConfigureModal = ({ availableNodes = [], }: EmailConfigureModalProps) => { const { t } = useTranslation() + const subjectId = useId() const { data: email } = useSuspenseQuery({ ...userProfileQueryOptions(), select: (data) => data.profile.email, @@ -116,15 +117,19 @@ const EmailConfigureModal = ({
-
+
+ setSubject(e.target.value)} + onValueChange={setSubject} placeholder={t( ($) => $[`${i18nPrefix}.deliveryMethod.emailConfigure.subjectPlaceholder`], { ns: 'workflow' }, diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx index 86f772b15e7..5d730e134cc 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx @@ -45,6 +45,8 @@ type EmailSenderModalProps = { availableNodes?: Node[] } +type EmailSenderContentProps = Omit + const getOriginVar = (valueSelector: string[], list: NodeOutPutVar[]) => { const targetVar = list.find((item) => item.nodeId === valueSelector[0]) if (!targetVar) return undefined @@ -117,10 +119,9 @@ const formatEmailSenderInputs = ( } } -const EmailSenderModal = ({ +const EmailSenderContent = ({ nodeId, deliveryId, - open, onOpenChange, jumpToEmailConfigModal, config, @@ -128,7 +129,7 @@ const EmailSenderModal = ({ formInputs, nodesOutputVars = [], availableNodes = [], -}: EmailSenderModalProps) => { +}: EmailSenderContentProps) => { const { t } = useTranslation() const { data: userProfileEmail } = useSuspenseQuery({ ...userProfileQueryOptions(), @@ -258,142 +259,49 @@ const EmailSenderModal = ({ if (done) { return ( - - -
- - {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.done`], { ns: 'workflow' })} - - {debugEnabled && ( -
- $[`${i18nPrefix}.deliveryMethod.emailSender.debugDone`]} - ns="workflow" - components={{ - email: , - }} - values={{ email: userProfileEmail }} - /> -
- )} - {!debugEnabled && onlyWholeTeam && ( -
- $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone2`]} - ns="workflow" - components={{ - team: , - }} - values={{ team: currentWorkspace.name.replace(/'/g, '’') }} - /> -
- )} - {!debugEnabled && onlySpecificUsers && ( -
- {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone3`], { - ns: 'workflow', - })} -
- )} - {!debugEnabled && combinedRecipients && ( -
- $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone1`]} - ns="workflow" - components={{ - team: , - }} - values={{ team: currentWorkspace.name.replace(/'/g, '’') }} - /> -
- )} -
- {(onlySpecificUsers || combinedRecipients) && !debugEnabled && ( -
- +
+ + {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.done`], { ns: 'workflow' })} + + {debugEnabled && ( +
+ $[`${i18nPrefix}.deliveryMethod.emailSender.debugDone`]} + ns="workflow" + components={{ + email: , + }} + values={{ email: userProfileEmail }} />
)} -
- -
- -
- ) - } - - return ( - - - $['operation.close'], { ns: 'common' })} - size="lg" - className="absolute inset-e-6 top-6" - > - - - } - /> -
- - {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.title`], { ns: 'workflow' })} - - {debugEnabled && ( - <> -
- {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip`], { - ns: 'workflow', - })} -
-
- $[`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip2`]} - ns="workflow" - components={{ - email: , - }} - values={{ email: userProfileEmail }} - /> -
- - )} {!debugEnabled && onlyWholeTeam && ( -
+
$[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip2`]} + i18nKey={($) => $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone2`]} ns="workflow" components={{ - team: , + team: , }} values={{ team: currentWorkspace.name.replace(/'/g, '’') }} />
)} {!debugEnabled && onlySpecificUsers && ( -
- {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip3`], { +
+ {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone3`], { ns: 'workflow', })}
)} {!debugEnabled && combinedRecipients && ( -
+
$[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip1`]} + i18nKey={($) => $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone1`]} ns="workflow" components={{ - team: , + team: , }} values={{ team: currentWorkspace.name.replace(/'/g, '’') }} /> @@ -401,94 +309,193 @@ const EmailSenderModal = ({ )}
{(onlySpecificUsers || combinedRecipients) && !debugEnabled && ( - <> -
- -
-
- $[`${i18nPrefix}.deliveryMethod.emailSender.tip`]} - ns="workflow" - components={{ - strong: ( -
- - )} - {/* vars */} - {generatedInputs.length > 0 && ( - <> -
- -
-
- -
- {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.varsTip`], { - ns: 'workflow', - })} -
- {!collapsed && ( -
- {generatedInputs.map((variable, index) => ( -
- handleValueChange(variable.variable, v)} - /> -
- ))} -
- )} -
- +
+ +
)}
- -
+ + ) + } + + return ( + <> + $['operation.close'], { ns: 'common' })} + size="lg" + className="absolute inset-e-6 top-6" + > + + + } + /> +
+ + {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.title`], { ns: 'workflow' })} + + {debugEnabled && ( + <> +
+ {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip`], { + ns: 'workflow', + })} +
+
+ $[`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip2`]} + ns="workflow" + components={{ + email: , + }} + values={{ email: userProfileEmail }} + /> +
+ + )} + {!debugEnabled && onlyWholeTeam && ( +
+ $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip2`]} + ns="workflow" + components={{ + team: , + }} + values={{ team: currentWorkspace.name.replace(/'/g, '’') }} + /> +
+ )} + {!debugEnabled && onlySpecificUsers && ( +
+ {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip3`], { + ns: 'workflow', + })} +
+ )} + {!debugEnabled && combinedRecipients && ( +
+ $[`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip1`]} + ns="workflow" + components={{ + team: , + }} + values={{ team: currentWorkspace.name.replace(/'/g, '’') }} + /> +
+ )} +
+ {(onlySpecificUsers || combinedRecipients) && !debugEnabled && ( + <> +
+ +
+
+ $[`${i18nPrefix}.deliveryMethod.emailSender.tip`]} + ns="workflow" + components={{ + strong: ( +
+ + )} + {/* vars */} + {generatedInputs.length > 0 && ( + <> +
+ +
+
+ +
+ {t(($) => $[`${i18nPrefix}.deliveryMethod.emailSender.varsTip`], { + ns: 'workflow', + })} +
+ {!collapsed && ( +
+ {generatedInputs.map((variable, index) => ( +
+ handleValueChange(variable.variable, v)} + /> +
+ ))} +
+ )} +
+ + )} +
+ + +
+ + ) +} + +const EmailSenderModal = ({ open, onOpenChange, ...props }: EmailSenderModalProps) => { + return ( + + + ) diff --git a/web/app/components/workflow/nodes/trigger-schedule/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/trigger-schedule/__tests__/panel.spec.tsx index edf8dbe62e5..a0c53bbdbfd 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/__tests__/panel.spec.tsx @@ -204,7 +204,12 @@ describe('TriggerSchedulePanel', () => { renderPanel('node-3', createData({ mode: 'cron' })) - fireEvent.change(screen.getByDisplayValue('0 0 * * *'), { target: { value: '*/5 * * * *' } }) + fireEvent.change( + screen.getByRole('textbox', { + name: 'workflow.nodes.triggerSchedule.cronExpression', + }), + { target: { value: '*/5 * * * *' } }, + ) expect(handleCronExpressionChange).toHaveBeenCalledWith('*/5 * * * *') }) @@ -255,7 +260,11 @@ describe('TriggerSchedulePanel', () => { panelProps={panelProps} />, ) - expect(screen.getByRole('textbox')).toHaveValue('') + expect( + screen.getByRole('textbox', { + name: 'workflow.nodes.triggerSchedule.cronExpression', + }), + ).toHaveValue('') }) it('should render the hourly minute selector when the frequency is hourly', async () => { diff --git a/web/app/components/workflow/nodes/trigger-schedule/panel.tsx b/web/app/components/workflow/nodes/trigger-schedule/panel.tsx index e12b13815a5..838e4d740e5 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/panel.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/panel.tsx @@ -1,10 +1,10 @@ import type { FC } from 'react' import type { ScheduleTriggerNodeType } from './types' import type { NodePanelProps } from '@/app/components/workflow/types' +import { Input } from '@langgenius/dify-ui/input' import * as React from 'react' import { useTranslation } from 'react-i18next' import TimePicker from '@/app/components/base/date-and-time-picker/time-picker' -import Input from '@/app/components/base/input' import Field from '@/app/components/workflow/nodes/_base/components/field' import FrequencySelector from './components/frequency-selector' import ModeToggle from './components/mode-toggle' @@ -18,6 +18,7 @@ const i18nPrefix = 'nodes.triggerSchedule' const Panel: FC> = ({ id, data }) => { const { t } = useTranslation() + const cronExpressionId = React.useId() const { inputs, setInputs, @@ -112,12 +113,16 @@ const Panel: FC> = ({ id, data }) => { {inputs.mode === 'cron' && (
-
) : ( diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/__tests__/slash.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/__tests__/slash.spec.tsx index a35ac10f899..7ef45fc1e97 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/__tests__/slash.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/__tests__/slash.spec.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vite-plus/test' import { AgentPromptSlashMenu } from '../slash' describe('AgentPromptSlashMenu', () => { diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx index d1fcfb76586..a04a731b2e8 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx @@ -471,7 +471,7 @@ function AgentVersionRestoreBar({ + )}
- {!isEmptySearch && canEdit !== false && ( + {actions && (
+
+ ) +} + +function SkillGridPagination({ + state, +}: { + state: Extract< + Extract['content'], + { kind: 'list' } + >['pagination'] +}) { + const { t } = useTranslation('common') + + if (state.status === 'none') return null + if (state.status === 'error') return + + return ( +
+ {t(($) => $.loading)} + +
+ ) +} + +function SkillGrid({ state }: SkillGridProps) { + const { t } = useTranslation('skill') + const isBusy = + state.status === 'pending' || + (state.status === 'error' && state.isRetrying) || + (state.status === 'ready' && state.isFetching) + const readyContent = state.status === 'ready' ? state.content : undefined + + return ( +
$['skillManagement.listLabel'])} aria-busy={isBusy}> + {state.status === 'pending' && ( +
+ +
)} - {!isPending && !isError && skills.length === 0 && ( + {state.status === 'error' && ( $['skillManagement.loadingError'])} + /> + )} + {readyContent?.kind === 'empty' && ( + $['skillManagement.emptySearch']) : t(($) => $['skillManagement.empty']) } /> )} - {!isPending && - !isError && - skills.map((skill) => ( - - ))} - {!isPending && !isError && isFetchingNextPage && } + {readyContent?.kind === 'list' && ( + <> + {readyContent.refresh.status === 'error' && ( + + )} + {/* Safari list semantics: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/list-style#accessibility */} + {/* oxlint-disable-next-line jsx-a11y/no-redundant-roles -- Dify's preflight removes list markers. */} +
    + {readyContent.skills.map((skill) => ( + + ))} +
+ + + )}
) } @@ -864,16 +959,91 @@ export default function SkillsPage() { const handleListScroll = (event: UIEvent) => { const target = event.currentTarget const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight - if (scrollBottom < 80 && hasNextPage && !isFetchingNextPage) void fetchNextPage() + if ( + scrollBottom < 80 && + hasNextPage && + !skillsQuery.isFetching && + !skillsQuery.isFetchNextPageError + ) + void fetchNextPage() } useEffect(() => { const viewport = listViewportRef.current - if (!viewport || viewport.clientHeight === 0 || isPending || isFetchingNextPage || !hasNextPage) + if ( + !viewport || + viewport.clientHeight === 0 || + isPending || + skillsQuery.isFetching || + skillsQuery.isFetchNextPageError || + !hasNextPage + ) return - if (viewport.scrollHeight - viewport.clientHeight < 80) void fetchNextPage() - }, [fetchNextPage, hasNextPage, isFetchingNextPage, isPending, skills.length]) + if (viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 80) + void fetchNextPage() + }, [ + fetchNextPage, + hasNextPage, + isPending, + skills.length, + skillsQuery.isFetching, + skillsQuery.isFetchNextPageError, + ]) + + const isFiltered = !!debouncedKeyword || selectedTags.length > 0 + const skillGridState: SkillGridState = isPending + ? { status: 'pending' } + : skillsQuery.isLoadingError || (skills.length === 0 && skillsQuery.isRefetchError) + ? { + status: 'error', + isRetrying: skillsQuery.isFetching, + onRetry: () => void skillsQuery.refetch(), + } + : { + status: 'ready', + content: + skills.length === 0 + ? { + kind: 'empty', + emptyState: isFiltered ? 'filtered' : 'skills', + actions: + canEdit && !isFiltered + ? { + creating: createMutation.isPending, + importing: importMutation.isPending, + onCreate: handleCreate, + onImport: () => importInputRef.current?.click(), + } + : undefined, + } + : { + kind: 'list', + skills, + pagination: skillsQuery.isFetchNextPageError + ? { + status: 'error', + isRetrying: isFetchingNextPage, + onRetry: () => void fetchNextPage(), + } + : isFetchingNextPage + ? { status: 'loading' } + : { status: 'none' }, + refresh: skillsQuery.isRefetchError + ? { + status: 'error', + isRetrying: skillsQuery.isFetching, + onRetry: () => void skillsQuery.refetch(), + } + : { status: 'none' }, + cardActions: { + canDelete, + canEdit, + onOpenTagManagement: () => setShowTagManagementModal(true), + }, + }, + isFetching: skillsQuery.isFetching, + } return (
@@ -911,21 +1081,7 @@ export default function SkillsPage() { onScroll={handleListScroll} > - 0} - isError={skillsQuery.isError} - isFetching={skillsQuery.isFetching} - isFetchingNextPage={skillsQuery.isFetchingNextPage} - isPending={skillsQuery.isPending} - onCreate={handleCreate} - onImport={() => importInputRef.current?.click()} - onOpenTagManagement={() => setShowTagManagementModal(true)} - /> + diff --git a/web/features/tag-management/__tests__/skill-card-tags.spec.tsx b/web/features/tag-management/__tests__/skill-card-tags.spec.tsx index c9ca8b69ede..9e5528fb654 100644 --- a/web/features/tag-management/__tests__/skill-card-tags.spec.tsx +++ b/web/features/tag-management/__tests__/skill-card-tags.spec.tsx @@ -2,7 +2,7 @@ import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types. import type { ComponentProps } from 'react' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { SkillCardTags } from '../components/skill-card-tags' const mocks = vi.hoisted(() => ({ diff --git a/web/i18n-config/__tests__/plural-selector.spec.ts b/web/i18n-config/__tests__/plural-selector.spec.ts index 9e98128f67e..ceeed3aeaa4 100644 --- a/web/i18n-config/__tests__/plural-selector.spec.ts +++ b/web/i18n-config/__tests__/plural-selector.spec.ts @@ -1,6 +1,6 @@ import type { SelectorParam } from 'i18next' import { createInstance } from 'i18next' -import { describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vite-plus/test' import agentV2 from '../../i18n/en-US/agent-v-2.json' import skill from '../../i18n/en-US/skill.json' import { getInitOptions } from '../settings' diff --git a/web/i18n/ar-TN/permission-keys.json b/web/i18n/ar-TN/permission-keys.json index 18321e3b376..a7f129455c5 100644 --- a/web/i18n/ar-TN/permission-keys.json +++ b/web/i18n/ar-TN/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "إدارة إعدادات امتداد API", "app.access_config": "تكوين أذونات الوصول إلى التطبيق", "app.acl.access_config": "عرض أذونات الوصول وإدارتها", - "app.acl.access_point_manage": "عرض نقاط الوصول وإدارتها", "app.acl.delete": "حذف التطبيق", "app.acl.deploy": "نشر التطبيق", "app.acl.edit": "تعديل معلومات التطبيق وتنسيقه", diff --git a/web/i18n/ar-TN/tools.json b/web/i18n/ar-TN/tools.json index 5f6bebe9d21..6a9ed92c7e1 100644 --- a/web/i18n/ar-TN/tools.json +++ b/web/i18n/ar-TN/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "الوصف", "createTool.toolOutput.reserved": "محجوز", "createTool.toolOutput.reservedParameterDuplicateTip": "text و json و files هي متغيرات محجوزة. لا يمكن أن تظهر المتغيرات بهذه الأسماء في مخطط الإخراج.", + "createTool.toolOutput.sourceNode": "العقدة المصدر", "createTool.toolOutput.title": "إخراج الأداة", "createTool.urlError": "يرجى إدخال عنوان URL صالح", "createTool.viewSchemaSpec": "عرض مواصفات OpenAPI-Swagger", diff --git a/web/i18n/de-DE/permission-keys.json b/web/i18n/de-DE/permission-keys.json index 2d546c0e082..fb32d92c699 100644 --- a/web/i18n/de-DE/permission-keys.json +++ b/web/i18n/de-DE/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "API-Erweiterungskonfiguration verwalten", "app.access_config": "App-Zugriffsberechtigungen konfigurieren", "app.acl.access_config": "Zugriffsberechtigungen anzeigen und verwalten", - "app.acl.access_point_manage": "Zugangspunkte anzeigen und verwalten", "app.acl.delete": "App löschen", "app.acl.deploy": "App bereitstellen", "app.acl.edit": "App-Informationen bearbeiten und App orchestrieren", diff --git a/web/i18n/de-DE/tools.json b/web/i18n/de-DE/tools.json index f4dcfd034ca..05d8aebdef2 100644 --- a/web/i18n/de-DE/tools.json +++ b/web/i18n/de-DE/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Beschreibung", "createTool.toolOutput.reserved": "Reserviert", "createTool.toolOutput.reservedParameterDuplicateTip": "Text, JSON und Dateien sind reservierte Variablen. Variablen mit diesen Namen dürfen im Ausgabeschema nicht erscheinen.", + "createTool.toolOutput.sourceNode": "Quellknoten", "createTool.toolOutput.title": "Werkzeugausgabe", "createTool.urlError": "Bitte geben Sie eine gültige URL ein", "createTool.viewSchemaSpec": "Die OpenAPI-Swagger-Spezifikation anzeigen", diff --git a/web/i18n/en-US/permission-keys.json b/web/i18n/en-US/permission-keys.json index e69f68bb0b3..2344caa11e1 100644 --- a/web/i18n/en-US/permission-keys.json +++ b/web/i18n/en-US/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Manage API extension configuration", "app.access_config": "Configure app access permissions", "app.acl.access_config": "View and manage access permissions", - "app.acl.access_point_manage": "View and manage access points", "app.acl.delete": "Delete app", "app.acl.deploy": "Deploy app", "app.acl.edit": "Edit app information and orchestrate app", diff --git a/web/i18n/en-US/tools.json b/web/i18n/en-US/tools.json index d03a74b9209..adaa5d12a78 100644 --- a/web/i18n/en-US/tools.json +++ b/web/i18n/en-US/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Description", "createTool.toolOutput.reserved": "Reserved", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json, and files are reserved variables. Variables with these names cannot appear in the output schema.", + "createTool.toolOutput.sourceNode": "Source node", "createTool.toolOutput.title": "Tool Output", "createTool.urlError": "Please enter a valid URL", "createTool.viewSchemaSpec": "View the OpenAPI-Swagger Specification", diff --git a/web/i18n/es-ES/permission-keys.json b/web/i18n/es-ES/permission-keys.json index d7d5e9d57c8..af3336ceea7 100644 --- a/web/i18n/es-ES/permission-keys.json +++ b/web/i18n/es-ES/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Gestionar la configuración de la extensión de API", "app.access_config": "Configurar los permisos de acceso de la app", "app.acl.access_config": "Ver y gestionar los permisos de acceso", - "app.acl.access_point_manage": "Ver y gestionar los puntos de acceso", "app.acl.delete": "Eliminar app", "app.acl.deploy": "Desplegar la app", "app.acl.edit": "Editar la información y orquestar la app", diff --git a/web/i18n/es-ES/tools.json b/web/i18n/es-ES/tools.json index 0fbc485adb6..7da12e6890a 100644 --- a/web/i18n/es-ES/tools.json +++ b/web/i18n/es-ES/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Descripción", "createTool.toolOutput.reserved": "Reservado", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json y files son variables reservadas. Las variables con estos nombres no pueden aparecer en el esquema de salida.", + "createTool.toolOutput.sourceNode": "Nodo de origen", "createTool.toolOutput.title": "Salida de la herramienta", "createTool.urlError": "Por favor, ingresa una URL válida", "createTool.viewSchemaSpec": "Ver la Especificación OpenAPI-Swagger", diff --git a/web/i18n/fa-IR/permission-keys.json b/web/i18n/fa-IR/permission-keys.json index 5e739bed336..372374ed3b8 100644 --- a/web/i18n/fa-IR/permission-keys.json +++ b/web/i18n/fa-IR/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "مدیریت پیکربندی افزونه API", "app.access_config": "پیکربندی مجوزهای دسترسی برنامه", "app.acl.access_config": "مشاهده و مدیریت مجوزهای دسترسی", - "app.acl.access_point_manage": "مشاهده و مدیریت نقاط دسترسی", "app.acl.delete": "حذف برنامه", "app.acl.deploy": "استقرار برنامه", "app.acl.edit": "ویرایش اطلاعات برنامه و هماهنگ‌سازی برنامه", diff --git a/web/i18n/fa-IR/tools.json b/web/i18n/fa-IR/tools.json index f09383c28e1..82f0c6b854f 100644 --- a/web/i18n/fa-IR/tools.json +++ b/web/i18n/fa-IR/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "توضیحات", "createTool.toolOutput.reserved": "رزرو شده", "createTool.toolOutput.reservedParameterDuplicateTip": "متن، JSON و فایل‌ها متغیرهای رزرو شده هستند. متغیرهایی با این نام‌ها نمی‌توانند در طرح خروجی ظاهر شوند.", + "createTool.toolOutput.sourceNode": "گره منبع", "createTool.toolOutput.title": "خروجی ابزار", "createTool.urlError": "لطفاً یک URL معتبر وارد کنید", "createTool.viewSchemaSpec": "مشاهده مشخصات OpenAPI-Swagger", diff --git a/web/i18n/fr-FR/permission-keys.json b/web/i18n/fr-FR/permission-keys.json index c547052586d..074da133fae 100644 --- a/web/i18n/fr-FR/permission-keys.json +++ b/web/i18n/fr-FR/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Gérer la configuration de l'extension API", "app.access_config": "Configurer les autorisations d'accès à l'application", "app.acl.access_config": "Afficher et gérer les autorisations d'accès", - "app.acl.access_point_manage": "Afficher et gérer les points d’accès", "app.acl.delete": "Supprimer l'application", "app.acl.deploy": "Déployer l'application", "app.acl.edit": "Modifier les informations et orchestrer l'application", diff --git a/web/i18n/fr-FR/tools.json b/web/i18n/fr-FR/tools.json index d97b0353288..d21da9776c8 100644 --- a/web/i18n/fr-FR/tools.json +++ b/web/i18n/fr-FR/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Description", "createTool.toolOutput.reserved": "Réservé", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json et files sont des variables réservées. Les variables portant ces noms ne peuvent pas apparaître dans le schéma de sortie.", + "createTool.toolOutput.sourceNode": "Nœud source", "createTool.toolOutput.title": "Sortie de l'outil", "createTool.urlError": "Veuillez entrer une URL valide", "createTool.viewSchemaSpec": "Voir la spécification OpenAPI-Swagger", diff --git a/web/i18n/hi-IN/permission-keys.json b/web/i18n/hi-IN/permission-keys.json index 0779d870342..314cbfba84b 100644 --- a/web/i18n/hi-IN/permission-keys.json +++ b/web/i18n/hi-IN/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "API एक्सटेंशन कॉन्फ़िगरेशन प्रबंधित करें", "app.access_config": "ऐप एक्सेस अनुमतियाँ कॉन्फ़िगर करें", "app.acl.access_config": "एक्सेस अनुमतियाँ देखें और प्रबंधित करें", - "app.acl.access_point_manage": "एक्सेस पॉइंट देखें और प्रबंधित करें", "app.acl.delete": "ऐप हटाएं", "app.acl.deploy": "ऐप डिप्लॉय करें", "app.acl.edit": "ऐप की जानकारी संपादित करें और ऐप को ऑर्केस्ट्रेट करें", diff --git a/web/i18n/hi-IN/tools.json b/web/i18n/hi-IN/tools.json index 045f9eb2653..ed716295e12 100644 --- a/web/i18n/hi-IN/tools.json +++ b/web/i18n/hi-IN/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "विवरण", "createTool.toolOutput.reserved": "आरक्षित", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json, और फाइलें आरक्षित वेरिएबल हैं। इन नामों वाले वेरिएबल आउटपुट स्कीमा में दिखाई नहीं दे सकते।", + "createTool.toolOutput.sourceNode": "स्रोत नोड", "createTool.toolOutput.title": "उपकरण आउटपुट", "createTool.urlError": "कृपया एक मान्य URL दर्ज करें", "createTool.viewSchemaSpec": "OpenAPI-Swagger विनिर्देश देखें", diff --git a/web/i18n/id-ID/permission-keys.json b/web/i18n/id-ID/permission-keys.json index 349bff75538..4b04ea96f00 100644 --- a/web/i18n/id-ID/permission-keys.json +++ b/web/i18n/id-ID/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Kelola konfigurasi ekstensi API", "app.access_config": "Konfigurasikan izin akses aplikasi", "app.acl.access_config": "Lihat dan kelola izin akses", - "app.acl.access_point_manage": "Lihat dan kelola titik akses", "app.acl.delete": "Hapus aplikasi", "app.acl.deploy": "Deploy aplikasi", "app.acl.edit": "Edit informasi aplikasi dan orkestrasikan aplikasi", diff --git a/web/i18n/id-ID/tools.json b/web/i18n/id-ID/tools.json index a3316811ffb..b0630bfaf6e 100644 --- a/web/i18n/id-ID/tools.json +++ b/web/i18n/id-ID/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Deskripsi", "createTool.toolOutput.reserved": "Dicadangkan", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json, dan file adalah variabel yang dicadangkan. Variabel dengan nama-nama ini tidak dapat muncul dalam skema keluaran.", + "createTool.toolOutput.sourceNode": "Node sumber", "createTool.toolOutput.title": "Keluaran Alat", "createTool.urlError": "Silakan masukkan URL yang valid", "createTool.viewSchemaSpec": "Lihat Spesifikasi OpenAPI-Swagger", diff --git a/web/i18n/it-IT/permission-keys.json b/web/i18n/it-IT/permission-keys.json index b5f3ebf8094..899adce084b 100644 --- a/web/i18n/it-IT/permission-keys.json +++ b/web/i18n/it-IT/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Gestisci la configurazione delle estensioni API", "app.access_config": "Configura i permessi di accesso all'app", "app.acl.access_config": "Visualizza e gestisci i permessi di accesso", - "app.acl.access_point_manage": "Visualizza e gestisci i punti di accesso", "app.acl.delete": "Elimina app", "app.acl.deploy": "Distribuisci app", "app.acl.edit": "Modifica le informazioni e orchestra l'app", diff --git a/web/i18n/it-IT/tools.json b/web/i18n/it-IT/tools.json index fc54a2f9c71..f0bc0f59d28 100644 --- a/web/i18n/it-IT/tools.json +++ b/web/i18n/it-IT/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Descrizione", "createTool.toolOutput.reserved": "Riservato", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json e files sono variabili riservate. Le variabili con questi nomi non possono comparire nello schema di output.", + "createTool.toolOutput.sourceNode": "Nodo di origine", "createTool.toolOutput.title": "Output dello strumento", "createTool.urlError": "Per favore inserisci un URL valido", "createTool.viewSchemaSpec": "Visualizza la Specifica OpenAPI-Swagger", diff --git a/web/i18n/ja-JP/permission-keys.json b/web/i18n/ja-JP/permission-keys.json index 53033e2dc10..1b0c567f0e8 100644 --- a/web/i18n/ja-JP/permission-keys.json +++ b/web/i18n/ja-JP/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "API拡張設定を管理", "app.access_config": "アプリアクセス権限を設定", "app.acl.access_config": "アクセス権限の表示と管理", - "app.acl.access_point_manage": "アクセスポイントの表示と管理", "app.acl.delete": "アプリを削除", "app.acl.deploy": "アプリをデプロイ", "app.acl.edit": "アプリ情報の編集とアプリのオーケストレーション", diff --git a/web/i18n/ja-JP/tools.json b/web/i18n/ja-JP/tools.json index 13d4bbbb51a..adca26f4676 100644 --- a/web/i18n/ja-JP/tools.json +++ b/web/i18n/ja-JP/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "説明", "createTool.toolOutput.reserved": "予約済み", "createTool.toolOutput.reservedParameterDuplicateTip": "text、json、および files は予約語です。これらの名前の変数は出力スキーマに表示することはできません。", + "createTool.toolOutput.sourceNode": "ソースノード", "createTool.toolOutput.title": "ツール出力", "createTool.urlError": "有効な URL を入力してください", "createTool.viewSchemaSpec": "OpenAPI/Swagger 仕様を表示", diff --git a/web/i18n/ko-KR/permission-keys.json b/web/i18n/ko-KR/permission-keys.json index 45517f6acb9..3a9981a602a 100644 --- a/web/i18n/ko-KR/permission-keys.json +++ b/web/i18n/ko-KR/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "API 확장 구성 관리", "app.access_config": "앱 접근 권한 구성", "app.acl.access_config": "접근 권한 보기 및 관리", - "app.acl.access_point_manage": "액세스 지점 보기 및 관리", "app.acl.delete": "앱 삭제", "app.acl.deploy": "앱 배포", "app.acl.edit": "앱 정보 편집 및 앱 오케스트레이션", diff --git a/web/i18n/ko-KR/tools.json b/web/i18n/ko-KR/tools.json index 85831b51635..702ffdcc9d0 100644 --- a/web/i18n/ko-KR/tools.json +++ b/web/i18n/ko-KR/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "설명", "createTool.toolOutput.reserved": "예약됨", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json, 파일은 예약된 변수입니다. 이러한 이름을 가진 변수는 출력 스키마에 나타날 수 없습니다.", + "createTool.toolOutput.sourceNode": "소스 노드", "createTool.toolOutput.title": "도구 출력", "createTool.urlError": "유효한 URL 을 입력하세요", "createTool.viewSchemaSpec": "OpenAPI-Swagger 명세 보기", diff --git a/web/i18n/lo-LA/permission-keys.json b/web/i18n/lo-LA/permission-keys.json index e04ef1f59ec..bfba51cc7e2 100644 --- a/web/i18n/lo-LA/permission-keys.json +++ b/web/i18n/lo-LA/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "ຈັດການການຕັ້ງຄ່າ API extension", "app.access_config": "ຕັ້ງຄ່າສິດການເຂົ້າເຖິງແອັບ", "app.acl.access_config": "ເບິ່ງ ແລະ ຈັດການສິດການເຂົ້າເຖິງ", - "app.acl.access_point_manage": "ເບິ່ງ ແລະ ຈັດການຈຸດເຂົ້າເຖິງ", "app.acl.delete": "ລຶບແອັບ", "app.acl.deploy": "ຕິດຕັ້ງແອັບ", "app.acl.edit": "ແກ້ໄຂຂໍ້ມູນແອັບ ແລະ ຈັດການລະບົບແອັບ", diff --git a/web/i18n/lo-LA/tools.json b/web/i18n/lo-LA/tools.json index 472c62f2de2..1f8c77ea9a0 100644 --- a/web/i18n/lo-LA/tools.json +++ b/web/i18n/lo-LA/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "ຄຳອະທິບາຍ", "createTool.toolOutput.reserved": "ສະຫງວນໄວ້", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json, ແລະ files ແມ່ນຕົວປ່ຽນທີ່ຖືກສະຫງວນໄວ້. ຕົວປ່ຽນທີ່ມີຊື່ເຫຼົ່ານີ້ບໍ່ສາມາດປາກົດໃນໂຄງສ້າງຜົນອອກ (output schema) ໄດ້.", + "createTool.toolOutput.sourceNode": "ໂຫນດຕົ້ນທາງ", "createTool.toolOutput.title": "ຜົນອອກຂອງເຄື່ອງມື (Tool Output)", "createTool.urlError": "ກະລຸນາປ້ອນ URL ທີ່ຖືກຕ້ອງ", "createTool.viewSchemaSpec": "ເບິ່ງຂໍ້ກຳນົດ OpenAPI-Swagger", diff --git a/web/i18n/nl-NL/permission-keys.json b/web/i18n/nl-NL/permission-keys.json index 8266b38ae22..94fdf9d182c 100644 --- a/web/i18n/nl-NL/permission-keys.json +++ b/web/i18n/nl-NL/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "API-extensieconfiguratie beheren", "app.access_config": "Toegangsrechten voor app configureren", "app.acl.access_config": "Toegangsrechten bekijken en beheren", - "app.acl.access_point_manage": "Toegangspunten bekijken en beheren", "app.acl.delete": "App verwijderen", "app.acl.deploy": "App implementeren", "app.acl.edit": "App-informatie bewerken en app orkestreren", diff --git a/web/i18n/nl-NL/tools.json b/web/i18n/nl-NL/tools.json index a145b9366cd..cc47263292e 100644 --- a/web/i18n/nl-NL/tools.json +++ b/web/i18n/nl-NL/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Description", "createTool.toolOutput.reserved": "Reserved", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json, and files are reserved variables. Variables with these names cannot appear in the output schema.", + "createTool.toolOutput.sourceNode": "Bronknooppunt", "createTool.toolOutput.title": "Tool Output", "createTool.urlError": "Please enter a valid URL", "createTool.viewSchemaSpec": "View the OpenAPI-Swagger Specification", diff --git a/web/i18n/pl-PL/permission-keys.json b/web/i18n/pl-PL/permission-keys.json index 392f470914e..55619735f16 100644 --- a/web/i18n/pl-PL/permission-keys.json +++ b/web/i18n/pl-PL/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Zarządzaj konfiguracją rozszerzenia API", "app.access_config": "Konfiguruj uprawnienia dostępu do aplikacji", "app.acl.access_config": "Wyświetlaj uprawnienia dostępu i zarządzaj nimi", - "app.acl.access_point_manage": "Wyświetlaj punkty dostępu i zarządzaj nimi", "app.acl.delete": "Usuń aplikację", "app.acl.deploy": "Wdróż aplikację", "app.acl.edit": "Edytuj informacje o aplikacji i orkiestruj aplikację", diff --git a/web/i18n/pl-PL/tools.json b/web/i18n/pl-PL/tools.json index 4d4f0add225..3ec851e1527 100644 --- a/web/i18n/pl-PL/tools.json +++ b/web/i18n/pl-PL/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Opis", "createTool.toolOutput.reserved": "Zarezerwowane", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json i pliki są zastrzeżonymi zmiennymi. Zmienne o tych nazwach nie mogą pojawiać się w schemacie wyjściowym.", + "createTool.toolOutput.sourceNode": "Węzeł źródłowy", "createTool.toolOutput.title": "Wynik narzędzia", "createTool.urlError": "Proszę podać prawidłowy URL", "createTool.viewSchemaSpec": "Zobacz specyfikację OpenAPI-Swagger", diff --git a/web/i18n/pt-BR/permission-keys.json b/web/i18n/pt-BR/permission-keys.json index 36dd03d5719..32dda95b517 100644 --- a/web/i18n/pt-BR/permission-keys.json +++ b/web/i18n/pt-BR/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Gerenciar configuração de extensão de API", "app.access_config": "Configurar permissões de acesso ao aplicativo", "app.acl.access_config": "Visualizar e gerenciar permissões de acesso", - "app.acl.access_point_manage": "Visualizar e gerenciar pontos de acesso", "app.acl.delete": "Excluir aplicativo", "app.acl.deploy": "Implantar aplicativo", "app.acl.edit": "Editar informações e orquestrar o aplicativo", diff --git a/web/i18n/pt-BR/tools.json b/web/i18n/pt-BR/tools.json index 2f1afe9bc8a..7562d10bdf9 100644 --- a/web/i18n/pt-BR/tools.json +++ b/web/i18n/pt-BR/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Descrição", "createTool.toolOutput.reserved": "Reservado", "createTool.toolOutput.reservedParameterDuplicateTip": "texto, json e arquivos são variáveis reservadas. Variáveis com esses nomes não podem aparecer no esquema de saída.", + "createTool.toolOutput.sourceNode": "Nó de origem", "createTool.toolOutput.title": "Saída da ferramenta", "createTool.urlError": "Digite uma URL válida", "createTool.viewSchemaSpec": "Ver a Especificação OpenAPI-Swagger", diff --git a/web/i18n/ro-RO/permission-keys.json b/web/i18n/ro-RO/permission-keys.json index 73225f7cd60..2610e185492 100644 --- a/web/i18n/ro-RO/permission-keys.json +++ b/web/i18n/ro-RO/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Gestionează configurația extensiei API", "app.access_config": "Configurează permisiunile de acces ale aplicației", "app.acl.access_config": "Vizualizează și gestionează permisiunile de acces", - "app.acl.access_point_manage": "Vizualizează și gestionează punctele de acces", "app.acl.delete": "Șterge aplicația", "app.acl.deploy": "Implementează aplicația", "app.acl.edit": "Editează informațiile aplicației și orchestrează aplicația", diff --git a/web/i18n/ro-RO/tools.json b/web/i18n/ro-RO/tools.json index cc54c2ba457..a4a542dde50 100644 --- a/web/i18n/ro-RO/tools.json +++ b/web/i18n/ro-RO/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Descriere", "createTool.toolOutput.reserved": "Rezervat", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json și fișiere sunt variabile rezervate. Variabilele cu aceste nume nu pot apărea în schema de ieșire.", + "createTool.toolOutput.sourceNode": "Nod sursă", "createTool.toolOutput.title": "Ieșire instrument", "createTool.urlError": "Vă rugăm să introduceți un URL valid", "createTool.viewSchemaSpec": "Vezi specificația OpenAPI-Swagger", diff --git a/web/i18n/ru-RU/permission-keys.json b/web/i18n/ru-RU/permission-keys.json index bd986d65e95..574f0e96add 100644 --- a/web/i18n/ru-RU/permission-keys.json +++ b/web/i18n/ru-RU/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Управление конфигурацией API-расширений", "app.access_config": "Настройка прав доступа к приложению", "app.acl.access_config": "Просмотр и управление правами доступа", - "app.acl.access_point_manage": "Просмотр и управление точками доступа", "app.acl.delete": "Удаление приложения", "app.acl.deploy": "Развертывание приложения", "app.acl.edit": "Редактирование информации о приложении и оркестрация приложения", diff --git a/web/i18n/ru-RU/tools.json b/web/i18n/ru-RU/tools.json index 041225f7480..44242d4221e 100644 --- a/web/i18n/ru-RU/tools.json +++ b/web/i18n/ru-RU/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Описание", "createTool.toolOutput.reserved": "Зарезервировано", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json и files — зарезервированные переменные. Переменные с этими именами не могут появляться в схеме вывода.", + "createTool.toolOutput.sourceNode": "Исходный узел", "createTool.toolOutput.title": "Вывод инструмента", "createTool.urlError": "Пожалуйста, введите действительный URL", "createTool.viewSchemaSpec": "Посмотреть спецификацию OpenAPI-Swagger", diff --git a/web/i18n/sl-SI/permission-keys.json b/web/i18n/sl-SI/permission-keys.json index 4a49494a2c2..544c6f92a8d 100644 --- a/web/i18n/sl-SI/permission-keys.json +++ b/web/i18n/sl-SI/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Upravljanje konfiguracije razširitve API", "app.access_config": "Konfiguracija dovoljenj za dostop do aplikacije", "app.acl.access_config": "Ogled in upravljanje dovoljenj za dostop", - "app.acl.access_point_manage": "Ogled in upravljanje dostopnih točk", "app.acl.delete": "Izbriši aplikacijo", "app.acl.deploy": "Uvedi aplikacijo", "app.acl.edit": "Uredi podatke o aplikaciji in orkestriraj aplikacijo", diff --git a/web/i18n/sl-SI/tools.json b/web/i18n/sl-SI/tools.json index 9cb9fb3ea83..3f1faf5298d 100644 --- a/web/i18n/sl-SI/tools.json +++ b/web/i18n/sl-SI/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Opis", "createTool.toolOutput.reserved": "Rezervirano", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json in datoteke so rezervirane spremenljivke. Spremenljivke s temi imeni se ne smejo pojaviti v izhodni shemi.", + "createTool.toolOutput.sourceNode": "Izvorno vozlišče", "createTool.toolOutput.title": "Izhod orodja", "createTool.urlError": "Prosimo, vnesite veljaven URL", "createTool.viewSchemaSpec": "Oglejte si OpenAPI-Swagger specifikacijo", diff --git a/web/i18n/th-TH/permission-keys.json b/web/i18n/th-TH/permission-keys.json index 0ad4047ef26..b7b9854abf0 100644 --- a/web/i18n/th-TH/permission-keys.json +++ b/web/i18n/th-TH/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "จัดการการกําหนดค่าส่วนขยาย API", "app.access_config": "กําหนดค่าสิทธิ์การเข้าถึงแอป", "app.acl.access_config": "ดูและจัดการสิทธิ์การเข้าถึง", - "app.acl.access_point_manage": "ดูและจัดการจุดเข้าถึง", "app.acl.delete": "ลบแอป", "app.acl.deploy": "ปรับใช้แอป", "app.acl.edit": "แก้ไขข้อมูลแอปและจัดวางแอป", diff --git a/web/i18n/th-TH/tools.json b/web/i18n/th-TH/tools.json index 42de755b0bd..841a1d83fab 100644 --- a/web/i18n/th-TH/tools.json +++ b/web/i18n/th-TH/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "คำอธิบาย", "createTool.toolOutput.reserved": "สงวน", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json และ files เป็นตัวแปรที่สงวนไว้ ไม่สามารถใช้ชื่อตัวแปรเหล่านี้ในโครงสร้างผลลัพธ์ได้", + "createTool.toolOutput.sourceNode": "โหนดต้นทาง", "createTool.toolOutput.title": "เอาต์พุตของเครื่องมือ", "createTool.urlError": "โปรดป้อน URL ที่ถูกต้อง", "createTool.viewSchemaSpec": "ดูข้อมูลจําเพาะของ OpenAPI-Swagger", diff --git a/web/i18n/tr-TR/permission-keys.json b/web/i18n/tr-TR/permission-keys.json index 781d9d00d38..36ba8ec9709 100644 --- a/web/i18n/tr-TR/permission-keys.json +++ b/web/i18n/tr-TR/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "API uzantısı yapılandırmasını yönet", "app.access_config": "Uygulama erişim izinlerini yapılandır", "app.acl.access_config": "Erişim izinlerini görüntüle ve yönet", - "app.acl.access_point_manage": "Erişim noktalarını görüntüle ve yönet", "app.acl.delete": "Uygulamayı sil", "app.acl.deploy": "Uygulamayı dağıt", "app.acl.edit": "Uygulama bilgilerini düzenle ve uygulamayı orkestre et", diff --git a/web/i18n/tr-TR/tools.json b/web/i18n/tr-TR/tools.json index f0202003032..7e005afa20b 100644 --- a/web/i18n/tr-TR/tools.json +++ b/web/i18n/tr-TR/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Açıklama", "createTool.toolOutput.reserved": "Ayrılmış", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json ve dosyalar ayrılmış değişkenlerdir. Bu isimlere sahip değişkenler çıktı şemasında yer alamaz.", + "createTool.toolOutput.sourceNode": "Kaynak düğüm", "createTool.toolOutput.title": "Araç Çıktısı", "createTool.urlError": "Geçerli bir URL girin", "createTool.viewSchemaSpec": "OpenAPI-Swagger Spesifikasyonunu Görüntüle", diff --git a/web/i18n/uk-UA/permission-keys.json b/web/i18n/uk-UA/permission-keys.json index 8cfd28d2548..861c83a4367 100644 --- a/web/i18n/uk-UA/permission-keys.json +++ b/web/i18n/uk-UA/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Керування конфігурацією розширення API", "app.access_config": "Налаштування дозволів доступу до застосунку", "app.acl.access_config": "Переглядати дозволи доступу та керувати ними", - "app.acl.access_point_manage": "Переглядати точки доступу та керувати ними", "app.acl.delete": "Видалити застосунок", "app.acl.deploy": "Розгорнути застосунок", "app.acl.edit": "Редагувати інформацію про застосунок та оркеструвати застосунок", diff --git a/web/i18n/uk-UA/tools.json b/web/i18n/uk-UA/tools.json index 195c4bb4bbb..ec464b021e5 100644 --- a/web/i18n/uk-UA/tools.json +++ b/web/i18n/uk-UA/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Опис", "createTool.toolOutput.reserved": "Зарезервовано", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json та файли є зарезервованими змінними. Змінні з такими іменами не можуть з’являтися в схемі вихідних даних.", + "createTool.toolOutput.sourceNode": "Вихідний вузол", "createTool.toolOutput.title": "Вихідні дані інструменту", "createTool.urlError": "Введіть дійсну URL-адресу", "createTool.viewSchemaSpec": "Переглянути специфікацію OpenAPI-Swagger", diff --git a/web/i18n/vi-VN/permission-keys.json b/web/i18n/vi-VN/permission-keys.json index 2290d6362e0..1e6662a9304 100644 --- a/web/i18n/vi-VN/permission-keys.json +++ b/web/i18n/vi-VN/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "Quản lý cấu hình phần mở rộng API", "app.access_config": "Cấu hình quyền truy cập ứng dụng", "app.acl.access_config": "Xem và quản lý quyền truy cập", - "app.acl.access_point_manage": "Xem và quản lý điểm truy cập", "app.acl.delete": "Xóa ứng dụng", "app.acl.deploy": "Triển khai ứng dụng", "app.acl.edit": "Chỉnh sửa thông tin và điều phối ứng dụng", diff --git a/web/i18n/vi-VN/tools.json b/web/i18n/vi-VN/tools.json index 2659332f2e4..2835d2fc7b5 100644 --- a/web/i18n/vi-VN/tools.json +++ b/web/i18n/vi-VN/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "Mô tả", "createTool.toolOutput.reserved": "Dành riêng", "createTool.toolOutput.reservedParameterDuplicateTip": "text, json và files là các biến dành riêng. Các biến có tên này không thể xuất hiện trong sơ đồ đầu ra.", + "createTool.toolOutput.sourceNode": "Nút nguồn", "createTool.toolOutput.title": "Đầu ra của công cụ", "createTool.urlError": "Vui lòng nhập URL hợp lệ", "createTool.viewSchemaSpec": "Xem chi tiết OpenAPI-Swagger", diff --git a/web/i18n/zh-Hans/permission-keys.json b/web/i18n/zh-Hans/permission-keys.json index db8184ee533..91d9afb2cc8 100644 --- a/web/i18n/zh-Hans/permission-keys.json +++ b/web/i18n/zh-Hans/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "管理API扩展", "app.access_config": "配置应用访问权限", "app.acl.access_config": "查看与管理访问权限", - "app.acl.access_point_manage": "查看与管理访问点", "app.acl.delete": "删除应用", "app.acl.deploy": "部署应用", "app.acl.edit": "编辑应用信息与编排应用", diff --git a/web/i18n/zh-Hans/tools.json b/web/i18n/zh-Hans/tools.json index 01a6659d229..884a568437b 100644 --- a/web/i18n/zh-Hans/tools.json +++ b/web/i18n/zh-Hans/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "描述", "createTool.toolOutput.reserved": "预留", "createTool.toolOutput.reservedParameterDuplicateTip": "text、json、files 是预留变量,这些名称的变量不能出现在 output_schema 中。", + "createTool.toolOutput.sourceNode": "来源节点", "createTool.toolOutput.title": "工具出参", "createTool.urlError": "请输入有效的 URL", "createTool.viewSchemaSpec": "查看 OpenAPI-Swagger 规范", diff --git a/web/i18n/zh-Hant/permission-keys.json b/web/i18n/zh-Hant/permission-keys.json index 8a74b37f086..43350a59ada 100644 --- a/web/i18n/zh-Hant/permission-keys.json +++ b/web/i18n/zh-Hant/permission-keys.json @@ -3,7 +3,6 @@ "api_extension.manage": "管理API擴充配置", "app.access_config": "配置應用訪問權限", "app.acl.access_config": "檢視與管理存取權限", - "app.acl.access_point_manage": "檢視與管理存取點", "app.acl.delete": "刪除應用", "app.acl.deploy": "部署應用", "app.acl.edit": "編輯應用資訊與編排應用", diff --git a/web/i18n/zh-Hant/tools.json b/web/i18n/zh-Hant/tools.json index 6298f30b22b..78c99ced8e5 100644 --- a/web/i18n/zh-Hant/tools.json +++ b/web/i18n/zh-Hant/tools.json @@ -93,6 +93,7 @@ "createTool.toolOutput.description": "描述", "createTool.toolOutput.reserved": "已保留", "createTool.toolOutput.reservedParameterDuplicateTip": "text、json 和 files 是保留變數。這些名稱的變數不能出現在輸出結構中。", + "createTool.toolOutput.sourceNode": "來源節點", "createTool.toolOutput.title": "工具輸出", "createTool.urlError": "請輸入有效的 URL", "createTool.viewSchemaSpec": "檢視 OpenAPI-Swagger 規範", diff --git a/web/next.config.ts b/web/next.config.ts index aff1c791d67..adceb7f630f 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -17,11 +17,6 @@ const nextConfig: NextConfig = { bundler: 'turbopack', }), }, - experimental: { - // TODO: Remove when the `typescript` package can point to TypeScript 7. - // Next.js resolves that package, while compiler-API consumers still require TypeScript 6. - useTypeScriptCli: false, - }, productionBrowserSourceMaps: false, // enable browser source map generation during the production build typescript: { // https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors diff --git a/web/plugins/eslint/index.js b/web/plugins/eslint/index.js index 09af24bbf7b..2fd685cb217 100644 --- a/web/plugins/eslint/index.js +++ b/web/plugins/eslint/index.js @@ -2,6 +2,7 @@ import consistentPlaceholders from './rules/consistent-placeholders.js' import i18nFlatKey from './rules/i18n-flat-key.js' import noExtraKeys from './rules/no-extra-keys.js' import preferTailwindIcons from './rules/prefer-tailwind-icons.js' +import requireTitleForTruncatedText from './rules/require-title-for-truncated-text.js' /** @type {import('eslint').ESLint.Plugin} */ const plugin = { @@ -14,6 +15,7 @@ const plugin = { 'i18n-flat-key': i18nFlatKey, 'no-extra-keys': noExtraKeys, 'prefer-tailwind-icons': preferTailwindIcons, + 'require-title-for-truncated-text': requireTitleForTruncatedText, }, } diff --git a/web/plugins/eslint/rules/fixtures/truncation.module.css b/web/plugins/eslint/rules/fixtures/truncation.module.css new file mode 100644 index 00000000000..fb81f59ed30 --- /dev/null +++ b/web/plugins/eslint/rules/fixtures/truncation.module.css @@ -0,0 +1,23 @@ +.singleLine { + @apply truncate; +} + +.multipleLines { + -webkit-line-clamp: 2; +} + +.noClamp { + line-clamp: none; +} + +.zeroClamp { + -webkit-line-clamp: 0; +} + +.unsetClamp { + line-clamp: unset !important; +} + +.regular { + overflow: hidden; +} diff --git a/web/plugins/eslint/rules/require-title-for-truncated-text.js b/web/plugins/eslint/rules/require-title-for-truncated-text.js new file mode 100644 index 00000000000..d0c77ba52d9 --- /dev/null +++ b/web/plugins/eslint/rules/require-title-for-truncated-text.js @@ -0,0 +1,743 @@ +import { readFileSync, statSync } from 'node:fs' +import { dirname, resolve } from 'node:path' + +const DEFAULT_TRUNCATION_CLASSES = new Set(['text-ellipsis', 'truncate']) +const cssModuleClassCache = new Map() +const UNSAFE_TITLE_EXPRESSION = Symbol('unsafe-title-expression') +const STYLE_WRAPPER_TYPES = new Set([ + 'ChainExpression', + 'TSAsExpression', + 'TSInstantiationExpression', + 'TSNonNullExpression', + 'TSSatisfiesExpression', + 'TSTypeAssertion', +]) + +function getJsxName(node) { + if (!node) return null + if (node.type === 'JSXIdentifier') return node.name + if (node.type === 'JSXMemberExpression') { + const objectName = getJsxName(node.object) + const propertyName = getJsxName(node.property) + return objectName && propertyName ? `${objectName}.${propertyName}` : null + } + if (node.type === 'JSXNamespacedName') { + const namespaceName = getJsxName(node.namespace) + const name = getJsxName(node.name) + return namespaceName && name ? `${namespaceName}:${name}` : null + } + return null +} + +function getAttribute(openingElement, attributeName) { + return openingElement.attributes.find( + (attribute) => + attribute.type === 'JSXAttribute' && + attribute.name.type === 'JSXIdentifier' && + attribute.name.name === attributeName, + ) +} + +function unwrapExpression(node) { + let current = node + while (current && STYLE_WRAPPER_TYPES.has(current.type)) current = current.expression + return current +} + +function getStaticString(node) { + const current = unwrapExpression(node) + if (!current) return null + if (current.type === 'Literal' && typeof current.value === 'string') return current.value + if (current.type === 'TemplateLiteral' && current.expressions.length === 0) + return current.quasis[0]?.value.cooked ?? current.quasis[0]?.value.raw ?? '' + return null +} + +function getUnprefixedClassName(token) { + let bracketDepth = 0 + let lastVariantSeparator = -1 + + for (let index = 0; index < token.length; index++) { + const character = token[index] + if (character === '[' || character === '(') bracketDepth++ + else if (character === ']' || character === ')') bracketDepth = Math.max(0, bracketDepth - 1) + else if (character === ':' && bracketDepth === 0) lastVariantSeparator = index + } + + return token + .slice(lastVariantSeparator + 1) + .replace(/^!/u, '') + .replace(/!$/u, '') +} + +function isTruncationClassToken(token) { + const className = getUnprefixedClassName(token) + if (DEFAULT_TRUNCATION_CLASSES.has(className)) return true + if (className.startsWith('line-clamp-') && className !== 'line-clamp-none') return true + + const normalizedArbitraryClass = className.replaceAll(' ', '').toLowerCase() + return ( + normalizedArbitraryClass === '[text-overflow:ellipsis]' || + /^\[(?:-webkit-)?line-clamp:(?!none(?:\]|$))[^\]]+\]$/u.test(normalizedArbitraryClass) + ) +} + +function stringContainsTruncationClass(value) { + return value.split(/\s+/u).some((token) => token && isTruncationClassToken(token)) +} + +function hasActiveLineClampDeclaration(cssText) { + const lineClampPattern = /(?:-webkit-)?line-clamp\s*:\s*([^;}]+)/giu + return [...cssText.matchAll(lineClampPattern)].some((match) => { + const value = (match[1] ?? '').trim() + return value !== '' && !/^(?:0|none|unset)\b/iu.test(value) + }) +} + +function getTruncatingCssModuleClassNames(cssText) { + const classNames = new Set() + const classBlockPattern = /\.([A-Z_a-z][\w-]*)\s*\{([^{}]*)\}/gu + + for (const match of cssText.matchAll(classBlockPattern)) { + const [, className, body = ''] = match + const hasTextEllipsis = /text-overflow\s*:\s*ellipsis\b/iu.test(body) + const hasLineClamp = hasActiveLineClampDeclaration(body) + const hasTruncationApply = [...body.matchAll(/@apply\s+([^;}]+)/gu)].some((applyMatch) => + stringContainsTruncationClass(applyMatch[1] ?? ''), + ) + + if (hasTextEllipsis || hasLineClamp || hasTruncationApply) classNames.add(className) + } + + return classNames +} + +function getCssModuleClassNames(cssModulePath) { + try { + const modifiedTime = statSync(cssModulePath).mtimeMs + const cached = cssModuleClassCache.get(cssModulePath) + if (cached?.modifiedTime === modifiedTime) return cached.classNames + + const classNames = getTruncatingCssModuleClassNames(readFileSync(cssModulePath, 'utf8')) + cssModuleClassCache.set(cssModulePath, { classNames, modifiedTime }) + return classNames + } catch { + return new Set() + } +} + +function getPropertyName(property) { + if (!property || property.type !== 'Property') return null + if (!property.computed && property.key.type === 'Identifier') return property.key.name + if (property.key.type === 'Literal' && typeof property.key.value === 'string') + return property.key.value + return null +} + +function getVariableByName(scope, name) { + let currentScope = scope + while (currentScope) { + const variable = currentScope.set.get(name) + if (variable) return variable + currentScope = currentScope.upper + } + return null +} + +function getVariableInitializer(identifier, sourceCode) { + const variable = getVariableByName(sourceCode.getScope(identifier), identifier.name) + if (!variable || variable.defs.length !== 1) return null + + const [definition] = variable.defs + if ( + definition.type !== 'Variable' || + definition.node.type !== 'VariableDeclarator' || + definition.node.id.type !== 'Identifier' || + !definition.node.init + ) + return null + + return { initializer: definition.node.init, variable } +} + +function expressionContainsTruncationClass( + node, + sourceCode, + cssModuleBindings, + seenVariables = new Set(), +) { + const current = unwrapExpression(node) + if (!current) return false + + const staticString = getStaticString(current) + if (staticString !== null) return stringContainsTruncationClass(staticString) + + switch (current.type) { + case 'Identifier': { + const resolvedVariable = getVariableInitializer(current, sourceCode) + if (!resolvedVariable || seenVariables.has(resolvedVariable.variable)) return false + const nextSeenVariables = new Set(seenVariables) + nextSeenVariables.add(resolvedVariable.variable) + return expressionContainsTruncationClass( + resolvedVariable.initializer, + sourceCode, + cssModuleBindings, + nextSeenVariables, + ) + } + case 'MemberExpression': { + const object = unwrapExpression(current.object) + if (object?.type !== 'Identifier') return false + const propertyName = current.computed + ? getStaticString(current.property) + : current.property.type === 'Identifier' + ? current.property.name + : null + const variable = getVariableByName(sourceCode.getScope(object), object.name) + return propertyName !== null && cssModuleBindings.get(variable)?.has(propertyName) + } + case 'TemplateLiteral': + return ( + current.quasis.some((quasi) => + stringContainsTruncationClass(quasi.value.cooked ?? quasi.value.raw), + ) || + current.expressions.some((expression) => + expressionContainsTruncationClass( + expression, + sourceCode, + cssModuleBindings, + seenVariables, + ), + ) + ) + case 'TaggedTemplateExpression': + return expressionContainsTruncationClass( + current.quasi, + sourceCode, + cssModuleBindings, + seenVariables, + ) + case 'CallExpression': + case 'NewExpression': + return ( + expressionContainsTruncationClass( + current.callee, + sourceCode, + cssModuleBindings, + seenVariables, + ) || + current.arguments.some((argument) => + argument.type === 'SpreadElement' + ? expressionContainsTruncationClass( + argument.argument, + sourceCode, + cssModuleBindings, + seenVariables, + ) + : expressionContainsTruncationClass( + argument, + sourceCode, + cssModuleBindings, + seenVariables, + ), + ) + ) + case 'ConditionalExpression': + return ( + expressionContainsTruncationClass( + current.consequent, + sourceCode, + cssModuleBindings, + seenVariables, + ) || + expressionContainsTruncationClass( + current.alternate, + sourceCode, + cssModuleBindings, + seenVariables, + ) + ) + case 'LogicalExpression': + case 'BinaryExpression': + return ( + expressionContainsTruncationClass( + current.left, + sourceCode, + cssModuleBindings, + seenVariables, + ) || + expressionContainsTruncationClass( + current.right, + sourceCode, + cssModuleBindings, + seenVariables, + ) + ) + case 'ArrayExpression': + return current.elements.some( + (element) => + element && + (element.type === 'SpreadElement' + ? expressionContainsTruncationClass( + element.argument, + sourceCode, + cssModuleBindings, + seenVariables, + ) + : expressionContainsTruncationClass( + element, + sourceCode, + cssModuleBindings, + seenVariables, + )), + ) + case 'ObjectExpression': + return current.properties.some((property) => { + if (property.type === 'SpreadElement') + return expressionContainsTruncationClass( + property.argument, + sourceCode, + cssModuleBindings, + seenVariables, + ) + + const propertyName = getPropertyName(property) + return ( + (propertyName !== null && stringContainsTruncationClass(propertyName)) || + expressionContainsTruncationClass( + property.value, + sourceCode, + cssModuleBindings, + seenVariables, + ) + ) + }) + case 'SequenceExpression': + return current.expressions.some((expression) => + expressionContainsTruncationClass(expression, sourceCode, cssModuleBindings, seenVariables), + ) + default: + return false + } +} + +function isActiveLineClampValue(node) { + const current = unwrapExpression(node) + if (!current) return false + if (current.type === 'Literal') { + if (current.value === null || current.value === 0) return false + if (typeof current.value === 'string') + return !['', '0', 'none', 'unset'].includes(current.value.trim().toLowerCase()) + return true + } + return current.type !== 'Identifier' || current.name !== 'undefined' +} + +function expressionContainsTruncationStyle(node, sourceCode, seenVariables = new Set()) { + const current = unwrapExpression(node) + if (!current) return false + + if (current.type === 'Identifier') { + const resolvedVariable = getVariableInitializer(current, sourceCode) + if (!resolvedVariable || seenVariables.has(resolvedVariable.variable)) return false + const nextSeenVariables = new Set(seenVariables) + nextSeenVariables.add(resolvedVariable.variable) + return expressionContainsTruncationStyle( + resolvedVariable.initializer, + sourceCode, + nextSeenVariables, + ) + } + + if (current.type === 'ConditionalExpression') { + return ( + expressionContainsTruncationStyle(current.consequent, sourceCode, seenVariables) || + expressionContainsTruncationStyle(current.alternate, sourceCode, seenVariables) + ) + } + + if (current.type === 'LogicalExpression') { + return ( + expressionContainsTruncationStyle(current.left, sourceCode, seenVariables) || + expressionContainsTruncationStyle(current.right, sourceCode, seenVariables) + ) + } + + if (current.type === 'ArrayExpression') { + return current.elements.some( + (element) => + element && + expressionContainsTruncationStyle( + element.type === 'SpreadElement' ? element.argument : element, + sourceCode, + seenVariables, + ), + ) + } + + if (current.type !== 'ObjectExpression') return false + + return current.properties.some((property) => { + if (property.type === 'SpreadElement') + return expressionContainsTruncationStyle(property.argument, sourceCode, seenVariables) + + const propertyName = getPropertyName(property) + if (propertyName === 'textOverflow' || propertyName === 'text-overflow') + return getStaticString(property.value)?.trim().toLowerCase() === 'ellipsis' + if ( + ['WebkitLineClamp', 'webkitLineClamp', 'lineClamp', '-webkit-line-clamp'].includes( + propertyName, + ) + ) + return isActiveLineClampValue(property.value) + return false + }) +} + +function hasTruncation(openingElement, sourceCode, cssModuleBindings) { + const classAttribute = + getAttribute(openingElement, 'className') ?? getAttribute(openingElement, 'class') + if (classAttribute?.value) { + if ( + classAttribute.value.type === 'Literal' && + typeof classAttribute.value.value === 'string' && + stringContainsTruncationClass(classAttribute.value.value) + ) + return true + if ( + classAttribute.value.type === 'JSXExpressionContainer' && + expressionContainsTruncationClass( + classAttribute.value.expression, + sourceCode, + cssModuleBindings, + ) + ) + return true + } + + const styleAttribute = getAttribute(openingElement, 'style') + return ( + styleAttribute?.value?.type === 'JSXExpressionContainer' && + expressionContainsTruncationStyle(styleAttribute.value.expression, sourceCode) + ) +} + +function isEmptyTitleAttribute(attribute) { + if (!attribute || !attribute.value) return true + if (attribute.value.type === 'Literal') + return typeof attribute.value.value !== 'string' || attribute.value.value.trim() === '' + if (attribute.value.type !== 'JSXExpressionContainer') return false + + const expression = unwrapExpression(attribute.value.expression) + if (!expression || expression.type === 'JSXEmptyExpression') return true + if (expression.type === 'Literal') + return expression.value === null || String(expression.value).trim() === '' + if (expression.type === 'TemplateLiteral' && expression.expressions.length === 0) + return (expression.quasis[0]?.value.cooked ?? '').trim() === '' + return expression.type === 'Identifier' && expression.name === 'undefined' +} + +function isRenderableTitleExpression(node) { + const current = unwrapExpression(node) + if (!current) return false + return ![ + 'ArrowFunctionExpression', + 'FunctionExpression', + 'JSXElement', + 'JSXEmptyExpression', + 'JSXFragment', + 'ObjectExpression', + 'SequenceExpression', + ].includes(current.type) +} + +function isSafeToDuplicateTitleExpression(node) { + const current = unwrapExpression(node) + if (!current) return false + + if (current.type === 'Identifier') return current.name !== 'undefined' + if (current.type === 'Literal') + return ( + current.value !== null && !['boolean', 'object', 'undefined'].includes(typeof current.value) + ) + if (current.type === 'TemplateLiteral' && current.expressions.length === 0) + return (current.quasis[0]?.value.cooked ?? '').trim() !== '' + return false +} + +function getMeaningfulChildren(element) { + return element.children.filter((child) => { + if (child.type === 'JSXText') return child.value.trim() !== '' + if (child.type === 'JSXExpressionContainer') + return child.expression.type !== 'JSXEmptyExpression' + return true + }) +} + +function getSingleTextChild(element) { + const meaningfulChildren = getMeaningfulChildren(element) + if (meaningfulChildren.length !== 1) return null + + const child = meaningfulChildren[0] + if (child.type === 'JSXElement') return getSingleTextChild(child) + return child +} + +function getExpressionTextChildCount(node) { + const current = unwrapExpression(node) + if (!current) return 0 + + if (current.type === 'JSXElement' || current.type === 'JSXFragment') + return getTextChildCount(current) + if (current.type === 'ConditionalExpression') + return Math.max( + getExpressionTextChildCount(current.consequent), + getExpressionTextChildCount(current.alternate), + ) + if (current.type === 'LogicalExpression') { + if (current.operator === '&&') return getExpressionTextChildCount(current.right) + return Math.max( + getExpressionTextChildCount(current.left), + getExpressionTextChildCount(current.right), + ) + } + if (current.type === 'ArrayExpression') { + let count = 0 + for (const element of current.elements) { + if (!element) continue + count += getExpressionTextChildCount( + element.type === 'SpreadElement' ? element.argument : element, + ) + if (count > 1) return count + } + return count + } + if (current.type === 'Literal') { + if (current.value === null || typeof current.value === 'boolean') return 0 + if (typeof current.value === 'string') return current.value.trim() === '' ? 0 : 1 + } + if (current.type === 'TemplateLiteral' && current.expressions.length === 0) + return (current.quasis[0]?.value.cooked ?? '').trim() === '' ? 0 : 1 + if (current.type === 'Identifier' && current.name === 'undefined') return 0 + return isRenderableTitleExpression(current) ? 1 : 0 +} + +function getTextChildCount(element) { + let count = 0 + + for (const child of getMeaningfulChildren(element)) { + if (child.type === 'JSXText') count++ + else if (child.type === 'JSXElement' || child.type === 'JSXFragment') + count += getTextChildCount(child) + else if (child.type === 'JSXExpressionContainer') + count += getExpressionTextChildCount(child.expression) + + if (count > 1) return count + } + + return count +} + +function hasMultipleTextChildren(openingElement) { + const element = openingElement.parent + return element?.type === 'JSXElement' && getTextChildCount(element) > 1 +} + +function getTitleFixTextFromAttribute(attribute, sourceCode) { + if (!attribute?.value) return null + if (attribute.value.type === 'Literal') { + if (typeof attribute.value.value !== 'string' || attribute.value.value.trim() === '') + return null + return `title=${JSON.stringify(attribute.value.value)}` + } + if (attribute.value.type !== 'JSXExpressionContainer') return null + if (!isSafeToDuplicateTitleExpression(attribute.value.expression)) return UNSAFE_TITLE_EXPRESSION + return `title={${sourceCode.getText(attribute.value.expression)}}` +} + +function getTitleFixText(openingElement, sourceCode) { + const element = openingElement.parent + if (!element || element.type !== 'JSXElement') return null + + const child = getSingleTextChild(element) + if (child?.type === 'JSXText') { + const value = child.value.trim().replace(/\s+/gu, ' ') + return value ? `title=${JSON.stringify(value)}` : null + } + let hasUnsafeTitleExpression = false + if (child?.type === 'JSXExpressionContainer') { + if (isSafeToDuplicateTitleExpression(child.expression)) + return `title={${sourceCode.getText(child.expression)}}` + hasUnsafeTitleExpression = true + } + + for (const attributeName of ['value', 'placeholder', 'content', 'label', 'name', 'aria-label']) { + const attributeFixText = getTitleFixTextFromAttribute( + getAttribute(openingElement, attributeName), + sourceCode, + ) + if (attributeFixText === UNSAFE_TITLE_EXPRESSION) { + hasUnsafeTitleExpression = true + continue + } + if (attributeFixText) return attributeFixText + } + + return hasUnsafeTitleExpression ? UNSAFE_TITLE_EXPRESSION : null +} + +function hasTitledSingleChild(openingElement) { + const element = openingElement.parent + if (!element || element.type !== 'JSXElement') return false + const meaningfulChildren = getMeaningfulChildren(element) + if (meaningfulChildren.length !== 1 || meaningfulChildren[0].type !== 'JSXElement') return null + const titleAttribute = getAttribute(meaningfulChildren[0].openingElement, 'title') + return !!titleAttribute && !isEmptyTitleAttribute(titleAttribute) +} + +function isTooltipTriggerOpening(openingElement, tooltipTriggerNames) { + const name = getJsxName(openingElement.name) + return name !== null && tooltipTriggerNames.has(name) +} + +function isInsideTooltipTrigger(openingElement, tooltipTriggerNames) { + let current = openingElement.parent + while (current) { + if ( + current.type === 'JSXElement' && + isTooltipTriggerOpening(current.openingElement, tooltipTriggerNames) + ) + return true + if ( + current.type === 'JSXAttribute' && + current.name.type === 'JSXIdentifier' && + current.name.name === 'render' && + current.parent?.type === 'JSXOpeningElement' && + isTooltipTriggerOpening(current.parent, tooltipTriggerNames) + ) + return true + current = current.parent + } + return false +} + +function getOwningVariableName(openingElement) { + let current = openingElement.parent + while (current) { + if (current.type === 'VariableDeclarator') + return current.id.type === 'Identifier' ? current.id.name : null + if ( + current.type === 'FunctionDeclaration' || + current.type === 'FunctionExpression' || + current.type === 'ArrowFunctionExpression' + ) + return null + current = current.parent + } + return null +} + +function collectReferencedIdentifiers(node, names) { + const current = unwrapExpression(node) + if (!current) return + if (current.type === 'Identifier') { + names.add(current.name) + return + } + if (current.type === 'ConditionalExpression') { + collectReferencedIdentifiers(current.consequent, names) + collectReferencedIdentifiers(current.alternate, names) + } else if (current.type === 'LogicalExpression') { + collectReferencedIdentifiers(current.left, names) + collectReferencedIdentifiers(current.right, names) + } +} + +/** @type {import('eslint').Rule.RuleModule} */ +export default { + meta: { + type: 'suggestion', + docs: { + description: 'Flag truncated JSX text for manual full-content disclosure review', + }, + messages: { + emptyTitle: + 'Review this truncated text against the disclosure policy; do not replace its existing title automatically.', + missingTitle: + 'Review this truncated text against the AUTO, COVERED, SKIP, and REVIEW disclosure policy.', + }, + schema: [], + }, + create(context) { + const candidates = [] + const renderIdentifiers = new Set() + const tooltipTriggerNames = new Set() + const cssModuleBindings = new Map() + + function recordCssModuleImport(node) { + if (typeof node.source.value !== 'string' || !node.source.value.endsWith('.module.css')) + return + const defaultSpecifier = node.specifiers.find( + (specifier) => specifier.type === 'ImportDefaultSpecifier', + ) + if (!defaultSpecifier) return + + const filename = context.filename || context.getFilename() + if (!filename || filename.startsWith('<')) return + const cssModulePath = resolve(dirname(filename), node.source.value) + const classNames = getCssModuleClassNames(cssModulePath) + const variable = context.sourceCode.getDeclaredVariables(defaultSpecifier)[0] + if (classNames.size > 0 && variable) cssModuleBindings.set(variable, classNames) + } + + return { + ImportDeclaration(node) { + recordCssModuleImport(node) + for (const specifier of node.specifiers) { + if ( + specifier.type === 'ImportSpecifier' && + (specifier.imported.name ?? specifier.imported.value) === 'TooltipTrigger' + ) + tooltipTriggerNames.add(specifier.local.name) + } + }, + JSXOpeningElement(node) { + if (isTooltipTriggerOpening(node, tooltipTriggerNames)) { + const renderAttribute = getAttribute(node, 'render') + if (renderAttribute?.value?.type === 'JSXExpressionContainer') + collectReferencedIdentifiers(renderAttribute.value.expression, renderIdentifiers) + } + + if ( + getAttribute(node, 'className') || + getAttribute(node, 'class') || + getAttribute(node, 'style') + ) + candidates.push(node) + }, + 'Program:exit': function () { + for (const openingElement of candidates) { + if (!hasTruncation(openingElement, context.sourceCode, cssModuleBindings)) continue + if (isInsideTooltipTrigger(openingElement, tooltipTriggerNames)) continue + + const owningVariableName = getOwningVariableName(openingElement) + if (owningVariableName && renderIdentifiers.has(owningVariableName)) continue + + if (hasMultipleTextChildren(openingElement)) continue + if (hasTitledSingleChild(openingElement)) continue + + const titleAttribute = getAttribute(openingElement, 'title') + if (titleAttribute && !isEmptyTitleAttribute(titleAttribute)) continue + + const fixText = getTitleFixText(openingElement, context.sourceCode) + if (fixText === UNSAFE_TITLE_EXPRESSION) continue + + context.report({ + node: titleAttribute ?? openingElement.name, + messageId: titleAttribute ? 'emptyTitle' : 'missingTitle', + }) + } + }, + } + }, +} diff --git a/web/plugins/eslint/rules/require-title-for-truncated-text.test.js b/web/plugins/eslint/rules/require-title-for-truncated-text.test.js new file mode 100644 index 00000000000..80fb1cbb62e --- /dev/null +++ b/web/plugins/eslint/rules/require-title-for-truncated-text.test.js @@ -0,0 +1,316 @@ +import assert from 'node:assert/strict' +import { resolve } from 'node:path' +import tsParser from '@typescript-eslint/parser' +import { Linter } from 'eslint' +import { it } from 'vitest' +import rule from './require-title-for-truncated-text.js' +import './fixtures/truncation.module.css' + +const plugin = { + rules: { + 'require-title-for-truncated-text': rule, + }, +} + +function verifyAndFix(code, filename = 'test.tsx') { + const linter = new Linter({ cwd: resolve('..') }) + return linter.verifyAndFix( + code, + [ + { + files: ['**/*.tsx'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaFeatures: { jsx: true }, + ecmaVersion: 'latest', + sourceType: 'module', + }, + }, + plugins: { dify: plugin }, + rules: { 'dify/require-title-for-truncated-text': 'warn' }, + }, + ], + { filename }, + ) +} + +it('accepts an explicit title and line-clamp-none', () => { + const result = verifyAndFix(` + const name = 'Dify' + export const Example = () => <> + {name} + {name} + + `) + + assert.equal(result.messages.length, 0) + assert.equal(result.fixed, false) +}) + +it('accepts Dify UI TooltipTrigger children and render elements', () => { + const result = verifyAndFix(` + import { TooltipTrigger as Trigger } from '@langgenius/dify-ui/tooltip' + const name = 'Dify' + export const Example = () => <> + {name} + {name}} /> + + `) + + assert.equal(result.messages.length, 0) + assert.equal(result.fixed, false) +}) + +it('reports static classes, class helpers, constants, and inline styles without fixing', () => { + const code = ` + const truncatedClassName = cn('min-w-0', condition && 'md:truncate') + export const Example = ({ itemName, label }) => <> + {itemName} +

{label}

+
Description
+ + ` + const result = verifyAndFix(code) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 3) + assert.ok(result.messages.every((message) => message.severity === 1)) + assert.equal(result.output, code) +}) + +it('resolves truncation variables from the active lexical scope', () => { + const shadowedParameterCode = ` + const className = 'truncate' + const style = { textOverflow: 'ellipsis' } + export const Example = ({ className, style, label }) => <> + {label} + {label} + + ` + const shadowedParameterResult = verifyAndFix(shadowedParameterCode) + + assert.equal(shadowedParameterResult.fixed, false) + assert.equal(shadowedParameterResult.messages.length, 0) + assert.equal(shadowedParameterResult.output, shadowedParameterCode) + + const nestedBindingResult = verifyAndFix(` + const className = 'regular' + const style = { color: 'red' } + export const Example = ({ label }) => { + const className = 'truncate' + const style = { textOverflow: 'ellipsis' } + return <> + {label} + {label} + + } + `) + + assert.equal(nestedBindingResult.fixed, false) + assert.equal(nestedBindingResult.messages.length, 2) + assert.ok(nestedBindingResult.messages.every((message) => message.severity === 1)) +}) + +it('reports an empty title without replacing it', () => { + const code = ` + export const Example = ({ name }) => {name} + ` + const result = verifyAndFix(code) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 1) + assert.equal(result.messages[0].severity, 1) + assert.equal(result.messages[0].messageId, 'emptyTitle') + assert.equal(result.output, code) +}) + +it('reports custom components without fixing them', () => { + const code = ` + export const Example = ({ name }) => ( + {name} + ) + ` + const result = verifyAndFix(code) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 1) + assert.equal(result.messages[0].severity, 1) + assert.equal(result.output, code) +}) + +it('ignores expressions that may execute user code', () => { + const code = ` + export const Example = ({ item }) => { + let index = 0 + return <> + {item.name} + {getLabel()} + {index++} + + + } + ` + const result = verifyAndFix(code) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 0) + assert.equal(result.output, code) +}) + +it('reports a single nested text child or a text-bearing prop without fixing', () => { + const result = verifyAndFix(` + export const Example = ({ name, content }) => <> +
{name}
+ + + `) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 2) + assert.ok(result.messages.every((message) => message.severity === 1)) +}) + +it('checks Dify UI single text but ignores multiple text children', () => { + const filename = resolve('../packages/dify-ui/src/example.tsx') + const result = verifyAndFix( + ` + export const Example = ({ primary, secondary }) => <> + {primary} + {primary}{secondary} +
+ {primary} + {secondary} +
+ + `, + filename, + ) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 1) + assert.equal(result.messages[0].severity, 1) +}) + +it('does not count non-text conditional children as additional text', () => { + const result = verifyAndFix(` + export const Example = ({ primary, showOverlay }) => ( + + {primary} + {showOverlay && } + + ) + `) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 1) + assert.equal(result.messages[0].messageId, 'missingTitle') +}) + +it('resolves truncation classes imported from CSS modules', () => { + const filename = resolve('plugins/eslint/rules/fixtures/example.tsx') + const result = verifyAndFix( + ` + import styles from './truncation.module.css' + export const Example = ({ name }) => <> + {name} + {name} + {name} + {name} + {name} + {name} + + export const Shadowed = ({ styles, name }) => ( + {name} + ) + `, + filename, + ) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 2) + assert.ok(result.messages.every((message) => message.severity === 1)) +}) + +it('reports dynamic truncation class and style expressions without fixing', () => { + const code = ` + const regularClassName = 'regular' + const dynamicClassNames = ['regular'] + const classMap = { regular: true } + const regularStyle = { color: 'red' } + const styles = [regularStyle] + const tag = strings => strings[0] + export const Example = ({ condition, name }) => <> + {name} + {name} + {name} + {name} + {name} + {name} + {name} + {name} + {name} + {name} + {name} + {name} + {name} + {name} + + ` + const result = verifyAndFix(code) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 11) + assert.ok(result.messages.every((message) => message.messageId === 'missingTitle')) + assert.equal(result.output, code) +}) + +it('reports statically empty title forms without replacing them', () => { + const code = ` + export const Example = ({ name }) => <> + {name} + {name} + {name} + {name} + {name} + + ` + const result = verifyAndFix(code) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 5) + assert.ok(result.messages.every((message) => message.messageId === 'emptyTitle')) + assert.equal(result.output, code) +}) + +it('reports safe literal content but accepts a titled single child', () => { + const result = verifyAndFix(` + export const Example = ({ name }) => <> + {42} + {\`Dify\`} + {name} +
{name}
+ + `) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 3) + assert.deepEqual( + result.messages.map((message) => message.messageId), + ['missingTitle', 'missingTitle', 'emptyTitle'], + ) +}) + +it('accepts referenced TooltipTrigger render candidates', () => { + const result = verifyAndFix(` + import { TooltipTrigger as Trigger } from '@langgenius/dify-ui/tooltip' + export const Example = ({ condition, name }) => { + const primaryTrigger = {name} + const fallbackTrigger = {name} + return + } + `) + + assert.equal(result.fixed, false) + assert.equal(result.messages.length, 0) +}) diff --git a/web/service/use-workflow.ts b/web/service/use-workflow.ts index 4d400bba27f..6a887e110d0 100644 --- a/web/service/use-workflow.ts +++ b/web/service/use-workflow.ts @@ -1,3 +1,4 @@ +import type { UseQueryOptions } from '@tanstack/react-query' import type { CommonResponse } from '@/models/common' import type { FlowType } from '@/types/common' import type { @@ -18,8 +19,13 @@ import { appWorkflowQueryOptions, appWorkflowVersionsInfiniteQueryKey } from './ const NAME_SPACE = 'workflow' -export const useAppWorkflow = (appID: string) => { - return useQuery(appWorkflowQueryOptions(appID)) +type UseAppWorkflowOptions = Pick + +export const useAppWorkflow = (appID: string, options?: UseAppWorkflowOptions) => { + return useQuery({ + ...appWorkflowQueryOptions(appID), + ...options, + }) } const WorkflowRunHistoryKey = [NAME_SPACE, 'runHistory'] diff --git a/web/utils/app-redirection.spec.ts b/web/utils/app-redirection.spec.ts index 521a500652d..d736ed5a428 100644 --- a/web/utils/app-redirection.spec.ts +++ b/web/utils/app-redirection.spec.ts @@ -14,20 +14,10 @@ describe('app-redirection', () => { * - App mode (workflow, advanced-chat, chat, completion, agent-chat) */ describe('getRedirectionPath', () => { - it('returns access point path when app access point permission is granted', () => { - const app = { - id: 'app-123', - mode: AppModeEnum.CHAT, - permission_keys: [AppACLPermission.AccessPoint], - } - const result = getRedirectionPath(app) - expect(result).toBe('/app/app-123/access-point') - }) - - it('returns apps list path when app ACL cannot access guarded pages or access point', () => { + it('returns access point path when app ACL cannot access guarded pages', () => { const app = { id: 'app-123', mode: AppModeEnum.CHAT, permission_keys: [] } const result = getRedirectionPath(app) - expect(result).toBe('/apps') + expect(result).toBe('/app/app-123/access-point') }) it('returns workflow path for workflow mode when app ACL can access layout', () => { @@ -102,11 +92,7 @@ describe('app-redirection', () => { }) it('handles different app IDs', () => { - const app1 = { - id: 'abc-123', - mode: AppModeEnum.CHAT, - permission_keys: [AppACLPermission.AccessPoint], - } + const app1 = { id: 'abc-123', mode: AppModeEnum.CHAT, permission_keys: [] } const app2 = { id: 'xyz-789', mode: AppModeEnum.WORKFLOW, @@ -143,7 +129,7 @@ describe('app-redirection', () => { const app = { id: 'app-123', mode: AppModeEnum.CHAT, - permission_keys: [AppACLPermission.AccessConfig, AppACLPermission.AccessPoint], + permission_keys: [AppACLPermission.AccessConfig], } expect(getRedirectionPath(app, { isRbacEnabled: false })).toBe('/app/app-123/access-point') @@ -187,12 +173,8 @@ describe('app-redirection', () => { /** * Tests that the redirection function is called with the correct path */ - it('calls redirection function with access point path when access point permission is granted', () => { - const app = { - id: 'app-123', - mode: AppModeEnum.CHAT, - permission_keys: [AppACLPermission.AccessPoint], - } + it('calls redirection function with access point path when app ACL cannot access guarded pages', () => { + const app = { id: 'app-123', mode: AppModeEnum.CHAT, permission_keys: [] } const mockRedirect = vi.fn() getRedirection(app, mockRedirect) diff --git a/web/utils/app-redirection.ts b/web/utils/app-redirection.ts index d73a350b3f3..4bdfce9a266 100644 --- a/web/utils/app-redirection.ts +++ b/web/utils/app-redirection.ts @@ -34,9 +34,7 @@ export const getRedirectionPath = ( if (app.mode === AppModeEnum.WORKFLOW && appACLCapabilities.canDeploy) return `/app/${app.id}/deploy` - if (appACLCapabilities.canAccessPoint) return `/app/${app.id}/access-point` - - return '/apps' + return `/app/${app.id}/access-point` } export const getRedirection = ( diff --git a/web/utils/permission.spec.ts b/web/utils/permission.spec.ts index 487c9c891b1..75dff3e7afe 100644 --- a/web/utils/permission.spec.ts +++ b/web/utils/permission.spec.ts @@ -50,15 +50,6 @@ describe('permission', () => { expect(releaseCapabilities.canDeploy).toBe(false) }) - it('keeps access point permission independent from other app ACL permissions', () => { - const accessPointCapabilities = getAppACLCapabilities([AppACLPermission.AccessPoint]) - const layoutCapabilities = getAppACLCapabilities([AppACLPermission.ViewLayout]) - - expect(accessPointCapabilities.canAccessPoint).toBe(true) - expect(accessPointCapabilities.canAccessLayout).toBe(false) - expect(layoutCapabilities.canAccessPoint).toBe(false) - }) - it('keeps monitor, tracing config, and log/annotation permissions independent', () => { const monitorCapabilities = getAppACLCapabilities([AppACLPermission.Monitor]) const tracingCapabilities = getAppACLCapabilities([AppACLPermission.TracingConfig]) @@ -118,7 +109,6 @@ describe('permission', () => { }) expect(capabilities.canViewLayout).toBe(true) - expect(capabilities.canAccessPoint).toBe(true) expect(capabilities.canTestAndRun).toBe(true) expect(capabilities.canEdit).toBe(true) expect(capabilities.canImportExportDSL).toBe(true) diff --git a/web/utils/permission.ts b/web/utils/permission.ts index 30b0b3e0cbe..063878b6607 100644 --- a/web/utils/permission.ts +++ b/web/utils/permission.ts @@ -2,7 +2,6 @@ import type { PermissionKey } from '@/models/access-control' export const AppACLPermission = { Preview: 'app.acl.preview', - AccessPoint: 'app.acl.access_point_manage', ViewLayout: 'app.acl.view_layout', TestAndRun: 'app.acl.test_and_run', Edit: 'app.acl.edit', @@ -39,7 +38,6 @@ export type ResourceMaintainerPermissionOptions = { } type AppACLCapabilities = { - canAccessPoint: boolean canViewLayout: boolean canTestAndRun: boolean canEdit: boolean @@ -137,11 +135,6 @@ export const getAppACLCapabilities = ( ) return { - canAccessPoint: hasResourcePermission( - permissionKeys, - AppACLPermission.AccessPoint, - hasMaintainerPermissions, - ), canViewLayout, canTestAndRun, canEdit,