diff --git a/platform/feedback/BUILD.bazel b/platform/feedback/BUILD.bazel
index a643cad38a05..46ec41344d2f 100644
--- a/platform/feedback/BUILD.bazel
+++ b/platform/feedback/BUILD.bazel
@@ -21,6 +21,7 @@ jvm_library(
deps = [
# do not sort,
"//platform/core-api:core",
+ "//platform/execution",
"//platform/projectModel-api:projectModel",
"//platform/util",
"//platform/editor-ui-api:editor-ui",
@@ -56,6 +57,7 @@ jvm_library(
# do not sort,
":feedback",
"//platform/core-api:core_test_lib",
+ "//platform/execution:execution_test_lib",
"//platform/projectModel-api:projectModel_test_lib",
"//platform/util:util_test_lib",
"//platform/editor-ui-api:editor-ui_test_lib",
diff --git a/platform/feedback/intellij.platform.feedback.iml b/platform/feedback/intellij.platform.feedback.iml
index 26d7552998ed..aeb62ef67520 100644
--- a/platform/feedback/intellij.platform.feedback.iml
+++ b/platform/feedback/intellij.platform.feedback.iml
@@ -30,6 +30,7 @@
+
diff --git a/platform/feedback/resources/intellij.platform.feedback.xml b/platform/feedback/resources/intellij.platform.feedback.xml
index 2310c814a50c..b8f0b3205da5 100644
--- a/platform/feedback/resources/intellij.platform.feedback.xml
+++ b/platform/feedback/resources/intellij.platform.feedback.xml
@@ -8,6 +8,7 @@
+
@@ -69,6 +70,14 @@
+
+
+
+
+
+
@@ -78,6 +87,7 @@
+
@@ -96,6 +106,9 @@
+
diff --git a/platform/feedback/resources/messages/WslSatisfactionFeedbackBundle.properties b/platform/feedback/resources/messages/WslSatisfactionFeedbackBundle.properties
new file mode 100644
index 000000000000..e3e94baa122e
--- /dev/null
+++ b/platform/feedback/resources/messages/WslSatisfactionFeedbackBundle.properties
@@ -0,0 +1,51 @@
+action.ShowWslSatisfactionDialog.text=Show WSL Satisfaction Notification
+
+wsl.satisfaction.notification.request.title=WSL feedback
+wsl.satisfaction.notification.request.content=Please help us improve the WSL experience by taking our short survey.
+
+wsl.satisfaction.dialog.top.title=WSL Support Experience Survey
+wsl.satisfaction.dialog.title=WSL support experience survey
+wsl.satisfaction.dialog.description=We are working on improving WSL support in {0} and would like to hear about your recent experience.
+
+wsl.satisfaction.dialog.rating.label=How satisfied are you with WSL support?
+wsl.satisfaction.dialog.rating.hint=Where 1 is "very dissatisfied" and 5 is "very satisfied".
+
+wsl.satisfaction.dialog.areas.label=How would you rate the following aspects of WSL support?
+wsl.satisfaction.dialog.areas.hint=Where 1 is "poor" and 5 is "excellent".
+wsl.satisfaction.dialog.area.stability=Stability (freezes)
+wsl.satisfaction.dialog.area.responsiveness=Responsiveness
+wsl.satisfaction.dialog.area.quality=Overall quality
+
+wsl.satisfaction.dialog.freeze.comparison.label=Compared to your previous experience, how often does the IDE freeze, hang, or become unresponsive when working with WSL in the current version?
+wsl.satisfaction.dialog.freeze.comparison.less=Less often
+wsl.satisfaction.dialog.freeze.comparison.same=About the same as before
+wsl.satisfaction.dialog.freeze.comparison.more=More often
+wsl.satisfaction.dialog.freeze.comparison.unsure=Hard to say
+
+wsl.satisfaction.dialog.freeze.frequency.label=How often do you currently experience IDE freezes when working with WSL?
+wsl.satisfaction.dialog.freeze.frequency.several.day=Several times a day
+wsl.satisfaction.dialog.freeze.frequency.once.day=About once a day
+wsl.satisfaction.dialog.freeze.frequency.several.week=Several times a week
+wsl.satisfaction.dialog.freeze.frequency.monthly=Once or several times a month
+wsl.satisfaction.dialog.freeze.frequency.rarely=Rarely or almost never
+
+wsl.satisfaction.dialog.freeze.situations.label=When do freezes usually occur, if at all?
+wsl.satisfaction.dialog.freeze.situations.none=I don't encounter freezes
+wsl.satisfaction.dialog.freeze.situations.opening=Opening a WSL project
+wsl.satisfaction.dialog.freeze.situations.indexing=Indexing
+wsl.satisfaction.dialog.freeze.situations.running=Running or debugging
+wsl.satisfaction.dialog.freeze.situations.terminal=Using the terminal
+wsl.satisfaction.dialog.freeze.situations.vcs=Git/VCS operations
+wsl.satisfaction.dialog.freeze.situations.docker=Docker or containers
+wsl.satisfaction.dialog.freeze.situations.editing=Editing files
+wsl.satisfaction.dialog.freeze.situations.branches=Switching branches
+wsl.satisfaction.dialog.freeze.situations.search=Searching/navigation
+
+wsl.satisfaction.dialog.additional.label=Is there anything else you would like to tell us about WSL support in {0}?
+
+wsl.satisfaction.thanks.feedback.content=Thank you for your feedback! It will help us improve WSL support.
+
+wsl.satisfaction.system.info.ide.name=IDE name:
+wsl.satisfaction.system.info.distribution=WSL distribution:
+wsl.satisfaction.system.info.wsl.version=WSL version:
+wsl.satisfaction.system.info.eel.api.used=WSL via IJent:
diff --git a/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/CheckBoxGroupBlock.kt b/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/CheckBoxGroupBlock.kt
index 2e44f25c59bf..cbb7d5d6266c 100644
--- a/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/CheckBoxGroupBlock.kt
+++ b/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/CheckBoxGroupBlock.kt
@@ -11,6 +11,7 @@ import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.RightGap
+import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.layout.selected
@@ -34,6 +35,7 @@ class CheckBoxGroupBlock(
private var myIncludeOtherTextField = false
private var myOtherProperty: String = ""
private var myOtherTextfieldPlaceholderText: String = ""
+ private var myColumnCount: Int = 1
private var otherCheckBox: JBCheckBox? = null
private var otherTextField: JBTextField? = null
@@ -41,23 +43,31 @@ class CheckBoxGroupBlock(
override fun addToPanel(panel: Panel) {
val allCheckBoxes = mutableListOf()
- panel.apply {
- buttonsGroup(indent = false) {
+ fun Row.addCheckBox(itemData: CheckBoxItemData) {
+ allCheckBoxes += checkBox(itemData.label)
+ .bindSelected({ itemData.property }, { itemData.property = it })
+ .addChooseOptionValidation(allCheckBoxes)
+ .component
+ }
+
+ val itemRows = myItemsData.chunked(myColumnCount)
+
+ val groupContent: (Panel) -> Unit = { container ->
+ container.buttonsGroup(indent = false) {
row {
label(myGroupLabel)
.bold()
}.bottomGap(BottomGap.NONE)
- myItemsData.forEachIndexed { i, itemData ->
- row {
- allCheckBoxes += checkBox(itemData.label).bindSelected(
- { myItemsData[i].property },
- { myItemsData[i].property = it }
- ).addChooseOptionValidation(allCheckBoxes)
- .component
- }.apply {
- if (i == myItemsData.size - 1 && !myIncludeOtherTextField) {
- this.bottomGap(BottomGap.MEDIUM)
- }
+ itemRows.forEachIndexed { rowIndex, rowItems ->
+ // Use the standard multi-column DSL helpers so the checkboxes align in evenly spaced columns.
+ val cells: List<(Row.() -> Unit)?> = rowItems.map { itemData -> { addCheckBox(itemData) } }
+ val addedRow = when (myColumnCount) {
+ 2 -> twoColumnsRow(cells.getOrNull(0), cells.getOrNull(1))
+ 3 -> threeColumnsRow(cells.getOrNull(0), cells.getOrNull(1), cells.getOrNull(2))
+ else -> row { rowItems.forEach { addCheckBox(it) } }
+ }
+ if (rowIndex == itemRows.lastIndex && !myIncludeOtherTextField) {
+ addedRow.bottomGap(BottomGap.MEDIUM)
}
}
@@ -109,6 +119,17 @@ class CheckBoxGroupBlock(
}
}
}
+
+ panel.apply {
+ if (myColumnCount > 1) {
+ // Isolate the multi-column grid in its own sub-panel so its wide columns do not force sibling
+ // blocks (e.g. rating rows) to align with them.
+ panel { groupContent(this) }
+ }
+ else {
+ groupContent(this)
+ }
+ }
}
override fun collectBlockTextDescription(stringBuilder: StringBuilder) {
@@ -151,6 +172,22 @@ class CheckBoxGroupBlock(
return this
}
+ /**
+ * Lays the checkboxes out over the given number of [columnCount] columns (1, 2 or 3) to keep tall option
+ * lists compact, using the standard [Panel.twoColumnsRow]/[Panel.threeColumnsRow] layout.
+ *
+ * Follow the IntelliJ UI guidelines when choosing the value:
+ * arrange checkboxes with labels of up to 30 characters in 2 columns,
+ * and checkboxes with labels of up to 15 characters in 3 columns.
+ *
+ * @see Layout guidelines
+ */
+ fun setColumnCount(columnCount: Int): CheckBoxGroupBlock {
+ require(columnCount in 1..3) { "columnCount must be 1, 2 or 3, but was $columnCount" }
+ myColumnCount = columnCount
+ return this
+ }
+
private fun Cell.addChooseOptionValidation(allCheckBoxes: List): Cell {
if (requireAnswer) {
val errorMessage = if (myIncludeOtherTextField) {
diff --git a/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/ComboBoxBlock.kt b/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/ComboBoxBlock.kt
index ff118f0c9ecf..7d2ffffb302b 100644
--- a/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/ComboBoxBlock.kt
+++ b/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/ComboBoxBlock.kt
@@ -8,10 +8,12 @@ import com.intellij.platform.feedback.impl.bundle.CommonFeedbackBundle
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.LabelPosition
+import com.intellij.ui.dsl.builder.MAX_LINE_LENGTH_WORD_WRAP
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.columns
import com.intellij.ui.dsl.builder.toMutableProperty
+import com.intellij.xml.util.XmlStringUtil
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
@@ -24,14 +26,26 @@ class ComboBoxBlock(@NlsContexts.Label private val myLabel: String,
private var myColumnSize: Int = COMBOBOX_COLUMN_SIZE
private var myRandomizeOptionOrder: Boolean = false
private var myUseAlignFill: Boolean = false
+ private var myUseWrappingLabel: Boolean = false
override fun addToPanel(panel: Panel) {
val items = if (myRandomizeOptionOrder) myItems.shuffled() else myItems
panel.apply {
+ // A long label rendered as a single-line JBLabel forces the whole dialog to become very wide,
+ // so it can optionally be shown as a word-wrapped label above the combo box instead.
+ if (myUseWrappingLabel) {
+ row {
+ text("${XmlStringUtil.escapeString(myLabel)}", maxLineLength = MAX_LINE_LENGTH_WORD_WRAP)
+ }.bottomGap(BottomGap.NONE)
+ }
row {
comboBox(items)
- .label(createBoldJBLabel(myLabel), LabelPosition.TOP)
+ .apply {
+ if (!myUseWrappingLabel) {
+ label(createBoldJBLabel(myLabel), LabelPosition.TOP)
+ }
+ }
.bindItem(::myProperty.toMutableProperty())
.apply {
if (myUseAlignFill) {
@@ -82,6 +96,15 @@ class ComboBoxBlock(@NlsContexts.Label private val myLabel: String,
return this
}
+ /**
+ * Renders the label as a word-wrapped text above the combo box instead of a single-line label,
+ * so a long question does not stretch the whole dialog horizontally.
+ */
+ fun useWrappingLabel(): ComboBoxBlock {
+ myUseWrappingLabel = true
+ return this
+ }
+
fun randomizeOptionOrder(): ComboBoxBlock {
myRandomizeOptionOrder = true
return this
diff --git a/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/RatingBlock.kt b/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/RatingBlock.kt
index 0ed838ea58d6..9744074d928e 100644
--- a/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/RatingBlock.kt
+++ b/platform/feedback/src/com/intellij/platform/feedback/dialog/uiBlocks/RatingBlock.kt
@@ -12,18 +12,35 @@ import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.Row
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
+import org.jetbrains.annotations.Nls
-class RatingBlock(@NlsContexts.Label private val myLabel: String,
- private val myJsonElementName: String) : FeedbackBlock, TextDescriptionProvider, JsonDataProvider {
+class RatingBlock(
+ @NlsContexts.Label private val myLabel: String,
+ private val myJsonElementName: String,
+) : FeedbackBlock, TextDescriptionProvider, JsonDataProvider {
private var myProperty: Int = 0
private var requireAnswer: Boolean = true
+ private var myHint: @Nls String? = null
override fun addToPanel(panel: Panel) {
+ val hint = myHint
panel.apply {
+ // With a hint, show the label and the hint above the rating (like RatingGroupBlock);
+ // otherwise keep the label attached on top of the rating for backward compatibility.
+ if (hint != null) {
+ row {
+ label(myLabel).bold()
+ rowComment(hint)
+ }.bottomGap(BottomGap.NONE)
+ }
row {
rating()
- .label(createBoldJBLabel(myLabel), LabelPosition.TOP)
+ .apply {
+ if (hint == null) {
+ label(createBoldJBLabel(myLabel), LabelPosition.TOP)
+ }
+ }
.apply {
onApply {
myProperty = this.component.myRating
@@ -57,6 +74,11 @@ class RatingBlock(@NlsContexts.Label private val myLabel: String,
requireAnswer = false
return this
}
+
+ fun setHint(@Nls hint: String): RatingBlock {
+ myHint = hint
+ return this
+ }
}
internal fun Row.rating(): Cell {
diff --git a/platform/feedback/src/com/intellij/platform/feedback/wsl/ShowWslSatisfactionDialogAction.kt b/platform/feedback/src/com/intellij/platform/feedback/wsl/ShowWslSatisfactionDialogAction.kt
new file mode 100644
index 000000000000..16813201d4c4
--- /dev/null
+++ b/platform/feedback/src/com/intellij/platform/feedback/wsl/ShowWslSatisfactionDialogAction.kt
@@ -0,0 +1,27 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.feedback.wsl
+
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.project.DumbAwareAction
+import java.util.function.Supplier
+
+/**
+ * Shows the WSL satisfaction survey notification (bypassing the usual show conditions) so QA can check
+ * both the notification wording and, via the "Respond" action, the dialog. Feedback is sent as a test request.
+ */
+@Suppress("ActionPresentationInstantiatedInCtor")
+internal class ShowWslSatisfactionDialogAction :
+ DumbAwareAction(Supplier { WslSatisfactionFeedbackBundle.message("action.ShowWslSatisfactionDialog.text") }) {
+
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ WslSatisfactionSurvey().showNotification(project, forTest = true)
+ }
+
+ override fun update(e: AnActionEvent) {
+ e.presentation.isEnabledAndVisible = e.project != null
+ }
+
+ override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
+}
diff --git a/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionFeedbackBundle.kt b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionFeedbackBundle.kt
new file mode 100644
index 000000000000..e035a9c555a6
--- /dev/null
+++ b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionFeedbackBundle.kt
@@ -0,0 +1,20 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.feedback.wsl
+
+import com.intellij.DynamicBundle
+import org.jetbrains.annotations.Nls
+import org.jetbrains.annotations.NonNls
+import org.jetbrains.annotations.PropertyKey
+
+internal object WslSatisfactionFeedbackBundle {
+
+ @NonNls
+ private const val BUNDLE = "messages.WslSatisfactionFeedbackBundle"
+
+ private val INSTANCE = DynamicBundle(WslSatisfactionFeedbackBundle::class.java, BUNDLE)
+
+ @Nls
+ fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String {
+ return INSTANCE.getMessage(key, *params)
+ }
+}
diff --git a/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionFeedbackDialog.kt b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionFeedbackDialog.kt
new file mode 100644
index 000000000000..a75759c2c0b1
--- /dev/null
+++ b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionFeedbackDialog.kt
@@ -0,0 +1,176 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.feedback.wsl
+
+import com.intellij.execution.wsl.WSLDistribution
+import com.intellij.execution.wsl.WslDistributionManager
+import com.intellij.execution.wsl.WslIjentAvailabilityService
+import com.intellij.openapi.application.ApplicationNamesInfo
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.wm.WindowManager
+import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialogWithEmail
+import com.intellij.platform.feedback.dialog.CommonFeedbackSystemData
+import com.intellij.platform.feedback.dialog.showFeedbackSystemInfoDialog
+import com.intellij.platform.feedback.dialog.uiBlocks.CheckBoxGroupBlock
+import com.intellij.platform.feedback.dialog.uiBlocks.CheckBoxItemData
+import com.intellij.platform.feedback.dialog.uiBlocks.ComboBoxBlock
+import com.intellij.platform.feedback.dialog.uiBlocks.DescriptionBlock
+import com.intellij.platform.feedback.dialog.uiBlocks.FeedbackBlock
+import com.intellij.platform.feedback.dialog.uiBlocks.RatingBlock
+import com.intellij.platform.feedback.dialog.uiBlocks.RatingGroupBlock
+import com.intellij.platform.feedback.dialog.uiBlocks.RatingItem
+import com.intellij.platform.feedback.dialog.uiBlocks.TextAreaBlock
+import com.intellij.platform.feedback.dialog.uiBlocks.TopLabelBlock
+import com.intellij.platform.feedback.impl.notification.ThanksForFeedbackNotification
+import com.intellij.ui.ScreenUtil
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.awt.Dimension
+import java.awt.Rectangle
+import java.nio.file.Path
+import javax.swing.JComponent
+
+internal class WslSatisfactionFeedbackDialog(
+ private val project: Project,
+ forTest: Boolean,
+) : BlockBasedFeedbackDialogWithEmail(project, forTest) {
+
+ /** Increase the additional number when feedback format is changed */
+ override val myFeedbackJsonVersion: Int = super.myFeedbackJsonVersion + 1
+
+ override val myFeedbackReportId: String = "wsl_satisfaction"
+ override val zendeskTicketTitle: String = "WSL Support in-IDE Feedback"
+ override val zendeskFeedbackType: String = "WSL Support Feedback"
+ override fun shouldAutoCloseZendeskTicket(): Boolean = false
+
+ private val ideName: String = ApplicationNamesInfo.getInstance().fullProductName
+
+ override suspend fun computeSystemInfoData(): WslSatisfactionSystemData {
+ val distribution = withContext(Dispatchers.IO) { findWslDistribution(project.basePath) }
+ val isEelApiUsed = WslIjentAvailabilityService.getInstance().runWslCommandsViaIjent()
+ return WslSatisfactionSystemData(
+ ideName = ideName,
+ wslDistribution = distribution?.presentableName ?: "UNKNOWN",
+ wslVersion = distribution?.version ?: -1,
+ isEelApiUsed = isEelApiUsed,
+ commonData = CommonFeedbackSystemData.getCurrentData(),
+ )
+ }
+
+ private fun findWslDistribution(projectPath: String?): WSLDistribution? {
+ if (projectPath == null) return null
+ return WslDistributionManager.getInstance().installedDistributions.firstOrNull {
+ runCatching { Path.of(projectPath).startsWith(Path.of(it.getUNCRootPath().toUri())) }.getOrDefault(false)
+ }
+ }
+
+ override fun showFeedbackSystemInfoDialog(systemInfoData: WslSatisfactionSystemData) {
+ showFeedbackSystemInfoDialog(myProject, systemInfoData.commonData) {
+ row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.ide.name")) {
+ label(systemInfoData.ideName)
+ }
+ row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.distribution")) {
+ label(systemInfoData.wslDistribution)
+ }
+ row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.wsl.version")) {
+ label(systemInfoData.wslVersion.toString())
+ }
+ row(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.system.info.eel.api.used")) {
+ label(systemInfoData.isEelApiUsed.toString())
+ }
+ }
+ }
+
+ override val myTitle: String = WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.top.title")
+
+ override val myBlocks: List = listOf(
+ TopLabelBlock(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.title")),
+ DescriptionBlock(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.description", ideName)),
+
+ RatingBlock(
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.rating.label"),
+ "satisfaction",
+ ).setHint(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.rating.hint")),
+
+ RatingGroupBlock(
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.areas.label"),
+ listOf(
+ RatingItem(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.area.stability"), "stability"),
+ RatingItem(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.area.responsiveness"), "responsiveness"),
+ RatingItem(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.area.quality"), "overall_quality"),
+ ),
+ ).setHint(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.areas.hint")),
+
+ ComboBoxBlock(
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.label"),
+ listOf(
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.less"),
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.same"),
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.more"),
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.comparison.unsure"),
+ ),
+ "freeze_comparison",
+ ).useWrappingLabel(),
+
+ ComboBoxBlock(
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.label"),
+ listOf(
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.several.day"),
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.once.day"),
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.several.week"),
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.monthly"),
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.frequency.rarely"),
+ ),
+ "freeze_frequency",
+ ).useWrappingLabel(),
+
+ CheckBoxGroupBlock(
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.label"),
+ listOf(
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.none"), "no_freezes"),
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.opening"), "opening_project"),
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.indexing"), "indexing"),
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.running"), "running_debugging"),
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.terminal"), "terminal"),
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.vcs"), "vcs"),
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.docker"), "docker"),
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.editing"), "editing"),
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.branches"), "switching_branches"),
+ CheckBoxItemData(WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.freeze.situations.search"), "search_navigation"),
+ ),
+ "freeze_situations",
+ ).setColumnCount(2).addOtherTextField(),
+
+ TextAreaBlock(
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.dialog.additional.label", ideName),
+ "additional_feedback",
+ ),
+ )
+
+ init {
+ init()
+ }
+
+ override fun createCenterPanel(): JComponent {
+ val centerPanel = super.createCenterPanel()
+ // The survey is long; cap the default height at 80% of the target screen (per the platform window-size
+ // guidelines) and let the content scroll, instead of growing the dialog to the full content height.
+ val maxHeight = (targetScreenBounds().height * 0.8).toInt()
+ val preferred = centerPanel.preferredSize
+ if (preferred.height > maxHeight) {
+ centerPanel.preferredSize = Dimension(preferred.width, maxHeight)
+ }
+ return centerPanel
+ }
+
+ /** Bounds of the screen the dialog will appear on (it is centered over the parent project frame). */
+ private fun targetScreenBounds(): Rectangle {
+ val frame = myProject?.let { WindowManager.getInstance().getFrame(it) }
+ return if (frame != null) ScreenUtil.getScreenRectangle(frame) else ScreenUtil.getMainScreenBounds()
+ }
+
+ override fun showThanksNotification() {
+ ThanksForFeedbackNotification(
+ description = WslSatisfactionFeedbackBundle.message("wsl.satisfaction.thanks.feedback.content")
+ ).notify(myProject)
+ }
+}
diff --git a/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurvey.kt b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurvey.kt
new file mode 100644
index 000000000000..7455bb7534f0
--- /dev/null
+++ b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurvey.kt
@@ -0,0 +1,12 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.feedback.wsl
+
+import com.intellij.platform.feedback.FeedbackSurvey
+import com.intellij.platform.feedback.FeedbackSurveyType
+import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
+import com.intellij.platform.feedback.InIdeFeedbackSurveyType
+
+internal class WslSatisfactionSurvey : FeedbackSurvey() {
+ override val feedbackSurveyType: FeedbackSurveyType =
+ InIdeFeedbackSurveyType(WslSatisfactionSurveyConfig())
+}
diff --git a/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurveyConfig.kt b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurveyConfig.kt
new file mode 100644
index 000000000000..d1f57e938354
--- /dev/null
+++ b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurveyConfig.kt
@@ -0,0 +1,72 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.feedback.wsl
+
+import com.intellij.openapi.application.ApplicationInfo
+import com.intellij.openapi.application.InitialConfigImportState
+import com.intellij.openapi.project.Project
+import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
+import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog
+import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
+import com.intellij.platform.feedback.impl.notification.RequestFeedbackNotification
+import com.intellij.util.PlatformUtils
+import com.intellij.util.system.LowLevelLocalMachineAccess
+import com.intellij.util.system.OS
+import kotlinx.datetime.LocalDate
+
+internal class WslSatisfactionSurveyConfig : InIdeFeedbackSurveyConfig {
+
+ override val surveyId: String = "wsl_satisfaction"
+
+ // Survey is targeted at 2026.2 + 2026.2.x; collect for ~two months after release. Adjust to the actual release date.
+ override val lastDayOfFeedbackCollection: LocalDate = LocalDate(2026, 10, 1)
+
+ override val requireIdeEAP: Boolean = false
+
+ @OptIn(LowLevelLocalMachineAccess::class)
+ override fun checkIdeIsSuitable(): Boolean {
+ // WSL exists only on Windows; the brief limits the survey to these IDEs.
+ return OS.CURRENT == OS.Windows &&
+ (PlatformUtils.isIdeaUltimate() ||
+ PlatformUtils.isIdeaCommunity() ||
+ PlatformUtils.isPyCharm() ||
+ PlatformUtils.isWebStorm() ||
+ PlatformUtils.isPhpStorm() ||
+ PlatformUtils.isGoIde())
+ }
+
+ override fun checkExtraConditionSatisfied(project: Project): Boolean {
+ if (!isTargetVersion()) return false
+ // Don't show for new users - only for existing users who upgraded to the new version.
+ if (InitialConfigImportState.isNewUser()) return false
+ if (!project.isWslProject()) return false
+ return WslSatisfactionSurveyStore.getInstance().shouldShowDialog()
+ }
+
+ private fun isTargetVersion(): Boolean {
+ return ApplicationInfo.getInstance().shortVersion == TARGET_VERSION
+ }
+
+ override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog {
+ return WslSatisfactionFeedbackDialog(project, forTest)
+ }
+
+ override fun updateStateAfterDialogClosedOk(project: Project) {
+ WslSatisfactionSurveyStore.getInstance().recordSurveyShown()
+ }
+
+ override fun createNotification(project: Project, forTest: Boolean): RequestFeedbackNotification {
+ return RequestFeedbackNotification(
+ "Feedback In IDE",
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.notification.request.title"),
+ WslSatisfactionFeedbackBundle.message("wsl.satisfaction.notification.request.content"),
+ )
+ }
+
+ override fun updateStateAfterNotificationShowed(project: Project) {
+ // do nothing
+ }
+
+ companion object {
+ private const val TARGET_VERSION: String = "2026.2"
+ }
+}
diff --git a/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurveyProjectActivity.kt b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurveyProjectActivity.kt
new file mode 100644
index 000000000000..e24a5b2f6da3
--- /dev/null
+++ b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurveyProjectActivity.kt
@@ -0,0 +1,16 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.feedback.wsl
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.startup.ProjectActivity
+
+/**
+ * Records that a WSL project was opened, feeding the "days working with WSL" counter used by
+ * [WslSatisfactionSurveyConfig] to decide when to show the WSL satisfaction survey.
+ */
+internal class WslSatisfactionSurveyProjectActivity : ProjectActivity {
+ override suspend fun execute(project: Project) {
+ if (!project.isWslProject()) return
+ WslSatisfactionSurveyStore.getInstance().recordWslProjectOpened()
+ }
+}
diff --git a/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurveyStore.kt b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurveyStore.kt
new file mode 100644
index 000000000000..472feec7080c
--- /dev/null
+++ b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSurveyStore.kt
@@ -0,0 +1,106 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.feedback.wsl
+
+import com.intellij.openapi.components.BaseState
+import com.intellij.openapi.components.PersistentStateComponent
+import com.intellij.openapi.components.RoamingType
+import com.intellij.openapi.components.State
+import com.intellij.openapi.components.Storage
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.util.system.LowLevelLocalMachineAccess
+import com.intellij.util.system.OS
+import java.time.Duration
+import java.time.Instant
+
+private val WSL_PATH_PREFIXES = listOf("//wsl$/", "//wsl.localhost/")
+
+/**
+ * Whether [this] project is opened from a WSL file system.
+ *
+ * Uses the same lightweight base-path prefix check as
+ * [com.intellij.featureStatistics.fusCollectors.WslUsagesCollector], so it can be called
+ * on any thread without touching the (potentially slow) distribution manager.
+ */
+@OptIn(LowLevelLocalMachineAccess::class)
+internal fun Project.isWslProject(): Boolean {
+ if (OS.CURRENT != OS.Windows) return false
+ val projectPath = guessProjectDir()?.path ?: return false
+ return WSL_PATH_PREFIXES.any { projectPath.startsWith(it) }
+}
+
+internal class WslSatisfactionSurveyState : BaseState() {
+ // Unix time seconds of the first detected WSL project open.
+ var firstWslUseTime: Long by property(0L)
+
+ // Number of distinct days on which a WSL project was open.
+ var daysWithWslProject: Int by property(0)
+
+ // Unix time seconds; [daysWithWslProject] is incremented at most once after this moment.
+ var nextCountedDay: Long by property(0L)
+ var userSawSurvey: Boolean by property(false)
+}
+
+@State(name = "WslSatisfactionSurveyStore", storages = [Storage(value = "wsl-survey.xml", roamingType = RoamingType.DISABLED)])
+internal class WslSatisfactionSurveyStore : PersistentStateComponent {
+
+ internal var currentState: WslSatisfactionSurveyState = WslSatisfactionSurveyState()
+
+ override fun getState(): WslSatisfactionSurveyState = currentState
+
+ override fun loadState(state: WslSatisfactionSurveyState) {
+ currentState = state
+ }
+
+ internal fun recordSurveyShown() {
+ currentState.userSawSurvey = true
+ }
+
+ /**
+ * Records that a WSL project was open now. Increments [WslSatisfactionSurveyState.daysWithWslProject]
+ * at most once per calendar day and stamps the first-usage time.
+ */
+ internal fun recordWslProjectOpened() {
+ val now = Instant.now()
+ if (now >= Instant.ofEpochSecond(currentState.nextCountedDay)) {
+ currentState.daysWithWslProject++
+ currentState.nextCountedDay = (now + Duration.ofDays(1)).epochSecond
+ }
+ if (currentState.firstWslUseTime == 0L) {
+ currentState.firstWslUseTime = now.epochSecond
+ }
+ }
+
+ internal fun shouldShowDialog(): Boolean {
+ val state = currentState
+ if (state.userSawSurvey) return false
+
+ val daysWithWslProject = Registry.intValue(REGISTRY_DAYS_WITH_WSL, -1).takeIf { it >= 0 } ?: state.daysWithWslProject
+ if (daysWithWslProject < MINIMUM_DAYS_WITH_WSL_PROJECT) return false
+
+ val firstWslUseTime = firstWslUseTime() ?: return false
+ return Duration.between(firstWslUseTime, Instant.now()) >= MINIMUM_DURATION_SINCE_FIRST_USE
+ }
+
+ private fun firstWslUseTime(): Instant? {
+ val override = Registry.stringValue(REGISTRY_FIRST_USE_DATE)
+ if (override.isNotBlank()) {
+ return runCatching { java.time.LocalDate.parse(override).atStartOfDay(java.time.ZoneOffset.UTC).toInstant() }.getOrNull()
+ }
+ if (currentState.firstWslUseTime == 0L) return null
+ return Instant.ofEpochSecond(currentState.firstWslUseTime)
+ }
+
+ companion object {
+ // "Project opened in WSL x3" + "after 3-5 days working with WSL" (IJPL-246739 brief).
+ internal const val MINIMUM_DAYS_WITH_WSL_PROJECT: Int = 3
+ internal val MINIMUM_DURATION_SINCE_FIRST_USE: Duration = Duration.ofDays(3)
+
+ internal const val REGISTRY_FIRST_USE_DATE: String = "wsl.survey.first.use.date"
+ internal const val REGISTRY_DAYS_WITH_WSL: String = "wsl.survey.days.with.wsl"
+
+ fun getInstance(): WslSatisfactionSurveyStore = service()
+ }
+}
diff --git a/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSystemData.kt b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSystemData.kt
new file mode 100644
index 000000000000..0002b61e6f57
--- /dev/null
+++ b/platform/feedback/src/com/intellij/platform/feedback/wsl/WslSatisfactionSystemData.kt
@@ -0,0 +1,25 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.feedback.wsl
+
+import com.intellij.openapi.util.NlsSafe
+import com.intellij.platform.feedback.dialog.CommonFeedbackSystemData
+import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.encodeToJsonElement
+
+/**
+ * Data attached automatically to the WSL satisfaction survey feedback, in addition to the answers:
+ * IDE name, WSL distribution and version, and whether WSL commands run via the IJent/EEL API.
+ */
+@Serializable
+internal data class WslSatisfactionSystemData(
+ @NlsSafe val ideName: String,
+ @NlsSafe val wslDistribution: String,
+ val wslVersion: Int,
+ val isEelApiUsed: Boolean,
+ val commonData: CommonFeedbackSystemData,
+) : SystemDataJsonSerializable {
+ override fun serializeToJson(json: Json): JsonElement = json.encodeToJsonElement(this)
+}