diff --git a/.agents/skills/how-to-write-component/SKILL.md b/.agents/skills/how-to-write-component/SKILL.md
index ee5c0d80887..2dcc0d7fc2d 100644
--- a/.agents/skills/how-to-write-component/SKILL.md
+++ b/.agents/skills/how-to-write-component/SKILL.md
@@ -49,6 +49,7 @@ Use this as the component decision guide for Dify web. Existing code is referenc
- Use uncontrolled `@langgenius/dify-ui/form` and `@langgenius/dify-ui/field` controls for edit/create forms whose fields are read only at submit time. Initialize query-backed defaults with `defaultValue` and keyed remounts.
- Promote form state to atoms only when another component must react to in-progress values, a draft must survive unmount/remount in the scoped workflow, or multiple steps share the same editable draft before submit.
- Treat `useParams`, route args, and `nuqs` query state as framework-owned state. When atom logic needs those values, hydrate primitive atoms at the route or surface boundary, such as with `useHydrateAtoms(..., { dangerouslyForceHydrate: true })`; keep URL updates in the route/query-state APIs instead of write atoms.
+- Within a route-owned feature, choose one source for route identity. If route params are bridged into feature atoms, use that bridge consistently for route-derived queries and actions instead of also threading the same route id through page, tab, and section props.
- For async work tied to atom state, use `atomWithQuery` or `atomWithMutation`; write atoms should update only the inputs that drive those atoms. This applies to pure frontend async work as well as network requests, so do not hand-roll loading/error/in-flight state with `useState` or `useRef` for atom-orchestrated async behavior. For component-owned remote work, use `useQuery` or `useMutation` directly.
- Row-local async state belongs to the row owner unless it participates in a shared Jotai workflow or needs atom-scoped reset semantics.
- Leave query and mutation atoms unscoped so they keep shared QueryClient cache and invalidation behavior. Scope resettable primitives and explicit hydration tuples; scope a derived atom only when every dependency should be private to that surface.
@@ -88,11 +89,13 @@ Use this as the component decision guide for Dify web. Existing code is referenc
- Keep `web/contract/*` as the API shape source of truth and follow the `{ params, query?, body? }` input shape.
- Consume generated queries with `useQuery(consoleQuery.xxx.queryOptions(...))` or `useQuery(marketplaceQuery.xxx.queryOptions(...))`.
+- If a generated query input comes from an atom, including a route-identity bridge atom, keep the query in `atomWithQuery`; do not unwrap the atom in a component just to call `useQuery`.
- Consume owner-local mutations with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))` when pending/error state is not consumed by feature atoms.
- In `atomWithQuery`, `atomWithInfiniteQuery`, and `atomWithMutation`, return generated `queryOptions()`, `infiniteOptions()`, or `mutationOptions()` directly. Pass `enabled`, `retry`, `placeholderData`, `select`, and pagination options into the generated call instead of spreading options into a hand-built object.
- For generated oRPC options with missing required input, branch the whole input with `input: condition ? validInput : skipToken` and `enabled: Boolean(condition)`. Never place `skipToken` inside a nested placeholder payload or coerce required IDs to `''`.
- When prefetch and render use the same request, extract local query options or a query-options atom so `prefetchQuery` and `useQuery`/`atomWithQuery` share the exact options.
- For custom query or mutation functions, wrap options with TanStack `queryOptions(...)` or `mutationOptions(...)`.
+- Do not extract generated `queryOptions(...)` into a helper solely to share input construction; extract only when prefetch/render must share exact options or the helper owns real domain behavior.
- Avoid pass-through hooks and thin `web/service/use-*` wrappers that only rename generated options. Keep feature hooks for real orchestration, workflow state, or shared domain behavior.
- Put shared cache behavior in `createTanstackQueryUtils(...experimental_defaults...)`. Component or atom callbacks may handle local toasts, closing dialogs, and navigation, but should not replace shared invalidation or patch shared server state locally.
- For overlays that may open heavier secondary content, prefetch from the trigger/menu open event with `queryClient.prefetchQuery(queryOptions)` when `onOpenChange` is available. Do not mount hidden subscribers just to warm cache.
diff --git a/web/app/(commonLayout)/deployments/[appInstanceId]/instances/page.tsx b/web/app/(commonLayout)/deployments/[appInstanceId]/instances/page.tsx
index d51bcfd5898..a8cd6e93c6d 100644
--- a/web/app/(commonLayout)/deployments/[appInstanceId]/instances/page.tsx
+++ b/web/app/(commonLayout)/deployments/[appInstanceId]/instances/page.tsx
@@ -1,8 +1,5 @@
import { DeployTab } from '@/features/deployments/detail/deploy-tab'
-export default async function InstanceDetailInstancesPage({ params }: {
- params: Promise<{ appInstanceId: string }>
-}) {
- const { appInstanceId } = await params
- return
+export default function InstanceDetailInstancesPage() {
+ return
}
diff --git a/web/app/(commonLayout)/deployments/[appInstanceId]/layout.tsx b/web/app/(commonLayout)/deployments/[appInstanceId]/layout.tsx
index 4e39b066201..1ad1bf7175c 100644
--- a/web/app/(commonLayout)/deployments/[appInstanceId]/layout.tsx
+++ b/web/app/(commonLayout)/deployments/[appInstanceId]/layout.tsx
@@ -1,14 +1,11 @@
import type { ReactNode } from 'react'
import { InstanceDetail } from '@/features/deployments/detail'
-export default async function InstanceDetailLayout({ children, params }: {
+export default function InstanceDetailLayout({ children }: {
children: ReactNode
- params: Promise<{ appInstanceId: string }>
}) {
- const { appInstanceId } = await params
-
return (
-
+
{children}
)
diff --git a/web/app/(commonLayout)/deployments/[appInstanceId]/overview/page.tsx b/web/app/(commonLayout)/deployments/[appInstanceId]/overview/page.tsx
index 7362d075a29..4683811a997 100644
--- a/web/app/(commonLayout)/deployments/[appInstanceId]/overview/page.tsx
+++ b/web/app/(commonLayout)/deployments/[appInstanceId]/overview/page.tsx
@@ -1,8 +1,5 @@
import { OverviewTab } from '@/features/deployments/detail/overview-tab'
-export default async function InstanceDetailOverviewPage({ params }: {
- params: Promise<{ appInstanceId: string }>
-}) {
- const { appInstanceId } = await params
- return
+export default function InstanceDetailOverviewPage() {
+ return
}
diff --git a/web/app/(commonLayout)/deployments/[appInstanceId]/releases/page.tsx b/web/app/(commonLayout)/deployments/[appInstanceId]/releases/page.tsx
index ee5e689cc65..3d77a24ae55 100644
--- a/web/app/(commonLayout)/deployments/[appInstanceId]/releases/page.tsx
+++ b/web/app/(commonLayout)/deployments/[appInstanceId]/releases/page.tsx
@@ -1,8 +1,5 @@
import { VersionsTab } from '@/features/deployments/detail/versions-tab'
-export default async function InstanceDetailReleasesPage({ params }: {
- params: Promise<{ appInstanceId: string }>
-}) {
- const { appInstanceId } = await params
- return
+export default function InstanceDetailReleasesPage() {
+ return
}
diff --git a/web/features/deployments/detail/__tests__/state.spec.ts b/web/features/deployments/detail/__tests__/state.spec.ts
index 1a53b843c4c..bf2694d178c 100644
--- a/web/features/deployments/detail/__tests__/state.spec.ts
+++ b/web/features/deployments/detail/__tests__/state.spec.ts
@@ -24,16 +24,6 @@ vi.mock('jotai-tanstack-query', () => ({
vi.mock('@/service/client', () => ({
consoleQuery: {
- apps: {
- byAppId: {
- get: {
- queryOptions: (options: QueryOptions) => ({
- ...options,
- queryKey: ['appById', options.input],
- }),
- },
- },
- },
enterprise: {
appInstanceService: {
getAppInstance: {
@@ -89,18 +79,13 @@ describe('deployment detail state', () => {
enabled: false,
input: skipToken,
})
- expect(store.get(state.deploymentSourceAppQueryAtom)).toMatchObject({
- enabled: false,
- input: skipToken,
- })
})
- it('should build detail query inputs from route and source identities', async () => {
+ it('should build detail query inputs from route identity', async () => {
const state = await loadState()
const store = createStore()
setDeploymentRoute(store)
- store.set(state.deploymentSourceAppIdAtom, 'source-app-1')
expect(store.get(state.deploymentDetailAppInstanceQueryAtom)).toMatchObject({
enabled: true,
@@ -117,10 +102,5 @@ describe('deployment detail state', () => {
input: { params: { appInstanceId: 'app-instance-1' } },
})
expect(environmentDeploymentsQuery.refetchInterval).toEqual(expect.any(Function))
-
- expect(store.get(state.deploymentSourceAppQueryAtom)).toMatchObject({
- enabled: true,
- input: { params: { app_id: 'source-app-1' } },
- })
})
})
diff --git a/web/features/deployments/detail/access-tab/channels/__tests__/section.spec.tsx b/web/features/deployments/detail/access-tab/channels/__tests__/section.spec.tsx
index 488ae1a7732..ab08477cc94 100644
--- a/web/features/deployments/detail/access-tab/channels/__tests__/section.spec.tsx
+++ b/web/features/deployments/detail/access-tab/channels/__tests__/section.spec.tsx
@@ -16,12 +16,17 @@ vi.mock('jotai', async (importOriginal) => {
}
})
-vi.mock('@tanstack/react-query', () => ({
- useMutation: () => ({
- isPending: false,
- mutate: mockToggleAccessChannel,
- }),
-}))
+vi.mock('@tanstack/react-query', async (importOriginal) => {
+ const actual = await importOriginal()
+
+ return {
+ ...actual,
+ useMutation: () => ({
+ isPending: false,
+ mutate: mockToggleAccessChannel,
+ }),
+ }
+})
vi.mock('@/service/client', () => ({
consoleQuery: {
diff --git a/web/features/deployments/detail/access-tab/permissions/__tests__/permissions.spec.tsx b/web/features/deployments/detail/access-tab/permissions/__tests__/permissions.spec.tsx
index 281c5f89bdd..ecdbc625769 100644
--- a/web/features/deployments/detail/access-tab/permissions/__tests__/permissions.spec.tsx
+++ b/web/features/deployments/detail/access-tab/permissions/__tests__/permissions.spec.tsx
@@ -20,22 +20,23 @@ vi.mock('jotai', async (importOriginal) => {
}
})
-vi.mock('@tanstack/react-query', () => ({
- useInfiniteQuery: () => ({
- data: { pages: [] },
- fetchNextPage: vi.fn(),
- isFetchingNextPage: false,
- isLoading: false,
- }),
- useMutation: () => ({
- isPending: false,
- mutate: mockMutate,
- }),
- useQuery: () => ({
- data: undefined,
- isPending: false,
- }),
-}))
+vi.mock('@tanstack/react-query', async (importOriginal) => {
+ const actual = await importOriginal()
+
+ return {
+ ...actual,
+ useInfiniteQuery: () => ({
+ data: { pages: [] },
+ fetchNextPage: vi.fn(),
+ isFetchingNextPage: false,
+ isLoading: false,
+ }),
+ useMutation: () => ({
+ isPending: false,
+ mutate: mockMutate,
+ }),
+ }
+})
vi.mock('@/service/client', () => ({
consoleQuery: {
diff --git a/web/features/deployments/detail/deploy-tab/deployment-environment-list.tsx b/web/features/deployments/detail/deploy-tab/deployment-environment-list.tsx
index fb45e113cb7..1319a539efa 100644
--- a/web/features/deployments/detail/deploy-tab/deployment-environment-list.tsx
+++ b/web/features/deployments/detail/deploy-tab/deployment-environment-list.tsx
@@ -72,8 +72,7 @@ function CurrentReleaseMobileSummary({ release }: {
)
}
-function DeploymentEnvironmentMobileRow({ appInstanceId, row }: {
- appInstanceId: string
+function DeploymentEnvironmentMobileRow({ row }: {
row: EnvironmentDeployment
}) {
const envId = row.environment.id
@@ -88,15 +87,14 @@ function DeploymentEnvironmentMobileRow({ appInstanceId, row }: {
{!isUndeployedDeploymentRow(row) && }