diff --git a/completeExample/src/main/kotlin/org/wip/complete/CompleteExamplePlugin.kt b/completeExample/src/main/kotlin/org/wip/complete/CompleteExamplePlugin.kt
index 3bb804ac..fcb26363 100644
--- a/completeExample/src/main/kotlin/org/wip/complete/CompleteExamplePlugin.kt
+++ b/completeExample/src/main/kotlin/org/wip/complete/CompleteExamplePlugin.kt
@@ -39,7 +39,9 @@ import java.io.File
data class CompleteExampleSettings(
@PluginSetting(
description = "Public configuration value example",
- defaultValue = "default_api_key"
+ defaultValue = "default_api_key",
+ minLength = 8,
+ semanticTypes = ["text/plain"]
) val apiKey: String? = "default_api_key",
@PluginSetting(
diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml
index 9303dcb6..ab0288cc 100644
--- a/composeApp/src/commonMain/composeResources/values-it/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml
@@ -1,5 +1,7 @@
+ Obbligatorie
+ Facoltative
PluginToolkit
Runner
Dashboard
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index ba373128..29bb810d 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -191,6 +191,8 @@
Actions
Custom Settings
Global Parameter Defaults
+ Required
+ Optional
Capability: %1$s
Configure required settings to unlock options
Locked capability: %1$s
@@ -474,4 +476,4 @@
Filter:
Sort:
Sync All
-
\ No newline at end of file
+
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/PaletteSidebar.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/PaletteSidebar.kt
index a2669e80..212a492b 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/PaletteSidebar.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/PaletteSidebar.kt
@@ -56,6 +56,7 @@ import androidx.navigation3.runtime.NavKey
import org.wip.plugintoolkit.features.navigation.GlobalRouter
import org.wip.plugintoolkit.features.navigation.model.Screen
import org.wip.plugintoolkit.features.plugin.logic.PluginManager
+import org.wip.plugintoolkit.features.plugin.model.resolveProvidedValues
import org.wip.plugintoolkit.features.plugin.ui.lockedClickInterceptor
import org.wip.plugintoolkit.shared.components.ToolkitTextField
import plugintoolkit.composeapp.generated.resources.Res
@@ -254,8 +255,9 @@ private fun CapabilitiesPalette(
)
)
caps.forEach { cap ->
- val isReady = remember(cap, settingsStore.settings, manifest?.settings) {
- cap.isReady(settingsStore.settings, manifest?.settings)
+ val providedSettings = settingsStore.resolveProvidedValues(manifest)
+ val isReady = remember(cap, providedSettings, manifest?.settings) {
+ cap.isReady(providedSettings, manifest?.settings)
}
val targetSettingKey = cap.requiredLocks.firstOrNull()
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/logic/PluginLifecycleManager.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/logic/PluginLifecycleManager.kt
index 6119f11e..4a76a74e 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/logic/PluginLifecycleManager.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/logic/PluginLifecycleManager.kt
@@ -21,6 +21,7 @@ import org.wip.plugintoolkit.core.utils.FileSystem
import org.wip.plugintoolkit.features.job.logic.JobManager
import org.wip.plugintoolkit.features.job.model.JobStatus
import org.wip.plugintoolkit.features.plugin.model.PluginSettingsStore
+import org.wip.plugintoolkit.features.plugin.model.resolveCustomSettings
import org.wip.plugintoolkit.features.settings.logic.SettingsRepository
import org.wip.plugintoolkit.features.settings.model.PluginUnplugBehavior
import org.wip.plugintoolkit.features.plugin.utils.PluginCompatibilityUtils
@@ -287,6 +288,7 @@ class PluginLifecycleManager(
}
val decryptedStore = store.copy(settings = decryptedSettings)
+ .withResolvedAutogeneratedSettings(manifest?.settings.orEmpty())
_pluginSettingsState.update { it + (pkg to decryptedStore) }
return decryptedStore
}
@@ -296,7 +298,8 @@ class PluginLifecycleManager(
val settingsFile = "${plugin.installPath}/settings.json"
val manifest = getManifest(pkg)
- val encryptedSettings = store.settings.mapValues { (key, value) ->
+ val resolvedStore = store.withResolvedAutogeneratedSettings(manifest?.settings.orEmpty())
+ val encryptedSettings = resolvedStore.settings.mapValues { (key, value) ->
val isSecret = manifest?.settings?.get(key)?.secret == true
if (isSecret && value is kotlinx.serialization.json.JsonPrimitive && value.isString) {
val encrypted = org.wip.plugintoolkit.core.utils.SecureStorage.encrypt(value.content)
@@ -305,12 +308,12 @@ class PluginLifecycleManager(
value
}
}
- val storeToSave = store.copy(settings = encryptedSettings)
+ val storeToSave = resolvedStore.copy(settings = encryptedSettings)
try {
fileSystem.writeFile(settingsFile, json.encodeToString(storeToSave))
// Update cache with the decrypted store
- _pluginSettingsState.update { it + (pkg to store) }
+ _pluginSettingsState.update { it + (pkg to resolvedStore) }
} catch (t: Throwable) {
Logger.e(t) { "Failed to save settings for $pkg" }
}
@@ -330,17 +333,10 @@ class PluginLifecycleManager(
val installPath = plugin?.installPath ?: ""
val jarFullPath = plugin?.let { "${it.installPath}/${it.jarFileName}" }
- val storedSettings = overriddenSettings ?: loadPluginSettings(pkg)
val actualManifest = manifest ?: getManifest(pkg)
- val mergedSettings = mutableMapOf()
-
- // 1. Manifest defaults
- actualManifest?.settings?.forEach { (key, meta) ->
- meta.defaultValue?.let { mergedSettings[key] = it }
- }
-
- // 2. User overrides
- mergedSettings.putAll(storedSettings.settings)
+ val storedSettings = (overriddenSettings ?: loadPluginSettings(pkg))
+ .withResolvedAutogeneratedSettings(actualManifest?.settings.orEmpty())
+ val mergedSettings = storedSettings.resolveCustomSettings(actualManifest)
val pluginLogger = jobManager.getPluginLogger(pkg, jobId)
val progressReporter = object : ProgressReporter {
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/logic/PluginSettingsResolver.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/logic/PluginSettingsResolver.kt
new file mode 100644
index 00000000..4c5a413b
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/logic/PluginSettingsResolver.kt
@@ -0,0 +1,63 @@
+package org.wip.plugintoolkit.features.plugin.logic
+
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import org.wip.plugintoolkit.api.DataType
+import org.wip.plugintoolkit.api.PrimitiveType
+import org.wip.plugintoolkit.api.SettingMetadata
+import org.wip.plugintoolkit.features.flows.logic.PathPatternResolver
+import org.wip.plugintoolkit.features.plugin.model.PluginSettingsStore
+import org.wip.plugintoolkit.features.plugin.utils.SettingsUtils
+
+internal fun resolveAutogeneratedSettings(
+ metadata: Map,
+ settings: Map,
+ additionalValues: Map = emptyMap()
+): Map {
+ val resolvedSettings = settings.toMutableMap()
+ val defaults = metadata.mapNotNull { (key, value) -> value.defaultValue?.let { key to it } }.toMap()
+
+ repeat(metadata.size.coerceAtLeast(1)) {
+ var changed = false
+ metadata.forEach { (key, settingMetadata) ->
+ val pattern = settingMetadata.autogeneratedPattern?.takeIf { it.isNotBlank() } ?: return@forEach
+ val availableValues = defaults + additionalValues + resolvedSettings
+ val stringValues = availableValues.mapValues { (valueKey, value) ->
+ val valueType = metadata[valueKey]?.type
+ if (valueType != null) SettingsUtils.jsonToString(value, valueType) else value.toString().trim('"')
+ }
+ val generated = runCatching { PathPatternResolver.tryResolve(pattern, stringValues) }.getOrNull()
+ ?: return@forEach
+ val generatedValue = generatedSettingValue(generated, settingMetadata.type) ?: return@forEach
+ if (resolvedSettings[key] != generatedValue) {
+ resolvedSettings[key] = generatedValue
+ changed = true
+ }
+ }
+ if (!changed) return resolvedSettings
+ }
+
+ return resolvedSettings
+}
+
+internal fun PluginSettingsStore.withResolvedAutogeneratedSettings(
+ metadata: Map
+): PluginSettingsStore {
+ val resolved = resolveAutogeneratedSettings(metadata, settings, globalParams)
+ return if (resolved == settings) this else copy(settings = resolved)
+}
+
+private fun generatedSettingValue(value: String, type: DataType): JsonElement? = when (type) {
+ is DataType.Primitive -> when (type.primitiveType) {
+ PrimitiveType.STRING, PrimitiveType.ANY, PrimitiveType.UNKNOWN -> JsonPrimitive(value)
+ PrimitiveType.BOOLEAN -> value.toBooleanStrictOrNull()?.let(::JsonPrimitive)
+ PrimitiveType.INT -> value.toIntOrNull()?.let(::JsonPrimitive)
+ PrimitiveType.LONG -> value.toLongOrNull()?.let(::JsonPrimitive)
+ PrimitiveType.SHORT -> value.toShortOrNull()?.let { JsonPrimitive(it.toInt()) }
+ PrimitiveType.BYTE -> value.toByteOrNull()?.let { JsonPrimitive(it.toInt()) }
+ PrimitiveType.DOUBLE -> value.toDoubleOrNull()?.let(::JsonPrimitive)
+ PrimitiveType.FLOAT -> value.toFloatOrNull()?.let { JsonPrimitive(it.toDouble()) }
+ PrimitiveType.UNIT -> null
+ }
+ else -> value.takeIf { it.isNotBlank() }?.let { runCatching { SettingsUtils.stringToJson(it, type) }.getOrNull() }
+}
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/model/PluginSettingsStore.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/model/PluginSettingsStore.kt
index 472b0a24..8ed2f01a 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/model/PluginSettingsStore.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/model/PluginSettingsStore.kt
@@ -2,6 +2,7 @@ package org.wip.plugintoolkit.features.plugin.model
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
+import org.wip.plugintoolkit.api.PluginManifest
@Serializable
data class PluginSettingsStore(
@@ -9,3 +10,15 @@ data class PluginSettingsStore(
val globalParams: Map = emptyMap(),
val capabilityParams: Map> = emptyMap()
)
+
+fun PluginManifest.defaultCustomSettings(): Map = settings.orEmpty().mapNotNull { (key, metadata) ->
+ metadata.defaultValue?.let { key to it }
+}.toMap()
+
+/** Manifest defaults with persisted user values taking precedence. */
+fun PluginSettingsStore.resolveCustomSettings(manifest: PluginManifest?): Map =
+ (manifest?.defaultCustomSettings() ?: emptyMap()) + settings
+
+/** Values available to generated inputs and lock evaluation in the settings UI. */
+fun PluginSettingsStore.resolveProvidedValues(manifest: PluginManifest?): Map =
+ resolveCustomSettings(manifest) + globalParams
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/DirectExecutionSidebar.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/DirectExecutionSidebar.kt
index a4ffd6e4..8eccd033 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/DirectExecutionSidebar.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/DirectExecutionSidebar.kt
@@ -50,6 +50,7 @@ import org.wip.plugintoolkit.api.Capability
import org.wip.plugintoolkit.api.PluginEntry
import org.wip.plugintoolkit.core.model.localized
import org.wip.plugintoolkit.core.theme.ToolkitTheme
+import org.wip.plugintoolkit.features.plugin.model.resolveProvidedValues
import org.wip.plugintoolkit.features.navigation.GlobalRouter
import org.wip.plugintoolkit.features.navigation.LocalGlobalRouter
import org.wip.plugintoolkit.features.navigation.model.Screen
@@ -172,7 +173,7 @@ fun DirectExecutionSidebar(
val manifest = plugin.getManifest().getOrThrow()
val pluginManager: org.wip.plugintoolkit.features.plugin.logic.PluginManager = org.koin.compose.koinInject()
val settingsStore = pluginManager.loadPluginSettings(pluginId)
- val settings = settingsStore.settings + settingsStore.globalParams
+ val settings = settingsStore.resolveProvidedValues(manifest)
val pluginLocksState by pluginManager.pluginLocksState.collectAsState()
val locks = pluginLocksState[pluginId] ?: pluginLocksState.values.fold(emptyMap()) { acc, map -> acc + map }
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/PluginContent.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/PluginContent.kt
index e8d0fd4b..53e97d1d 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/PluginContent.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/PluginContent.kt
@@ -40,15 +40,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
-import kotlinx.serialization.json.JsonPrimitive
-import org.wip.plugintoolkit.api.DataType
-import org.wip.plugintoolkit.api.PrimitiveType
import org.jetbrains.compose.resources.stringResource
import org.wip.plugintoolkit.api.Capability
import org.wip.plugintoolkit.api.ParameterRole
import org.wip.plugintoolkit.api.PluginManifest
import org.wip.plugintoolkit.features.job.model.BackgroundJob
import org.wip.plugintoolkit.features.job.model.JobStatus
+import org.wip.plugintoolkit.features.plugin.model.resolveProvidedValues
import org.wip.plugintoolkit.features.navigation.model.Screen
import org.wip.plugintoolkit.features.plugin.viewmodel.PluginViewModel
import org.wip.plugintoolkit.shared.components.plugin.JobResultCard
@@ -122,12 +120,8 @@ fun PluginContent(
val providedSettings = remember(pluginId, pluginSettingsState) {
val store = if (pluginId != null) pluginSettingsState[pluginId] ?: pluginManager.loadPluginSettings(pluginId) else null
val manifest = viewModel.selectedPlugin?.getManifest()?.getOrNull()
- val manifestDefaults = (manifest?.settings?.mapValues { (_, meta) ->
- meta.defaultValue ?: if (meta.type is DataType.Primitive && (meta.type as DataType.Primitive).primitiveType == PrimitiveType.BOOLEAN) {
- JsonPrimitive(false)
- } else null
- }?.filterValues { it != null } ?: emptyMap()) as Map
- manifestDefaults + (store?.settings ?: emptyMap()) + (store?.globalParams ?: emptyMap())
+ (store ?: org.wip.plugintoolkit.features.plugin.model.PluginSettingsStore())
+ .resolveProvidedValues(manifest)
}
if (selectedCapability == null) {
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/PluginSettingsContent.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/PluginSettingsContent.kt
index e723fa67..99c0dcfb 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/PluginSettingsContent.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/plugin/ui/PluginSettingsContent.kt
@@ -21,6 +21,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
@@ -51,6 +53,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -72,8 +75,10 @@ import org.wip.plugintoolkit.api.DataType
import org.wip.plugintoolkit.api.ParameterMetadata
import org.wip.plugintoolkit.api.PluginAction
import org.wip.plugintoolkit.api.PrimitiveType
+import org.wip.plugintoolkit.api.SettingMetadata
import org.wip.plugintoolkit.core.model.localized
import org.wip.plugintoolkit.core.theme.ToolkitTheme
+import org.wip.plugintoolkit.features.plugin.model.resolveCustomSettings
import org.wip.plugintoolkit.features.plugin.utils.SettingsUtils
import org.wip.plugintoolkit.features.plugin.viewmodel.PluginSettingsViewModel
import org.wip.plugintoolkit.shared.components.ToolkitChip
@@ -95,6 +100,8 @@ import plugintoolkit.composeapp.generated.resources.plugin_settings_by_section
import plugintoolkit.composeapp.generated.resources.plugin_settings_capability
import plugintoolkit.composeapp.generated.resources.plugin_settings_custom
import plugintoolkit.composeapp.generated.resources.plugin_settings_global_defaults
+import plugintoolkit.composeapp.generated.resources.plugin_settings_optional
+import plugintoolkit.composeapp.generated.resources.plugin_settings_required
import plugintoolkit.composeapp.generated.resources.settings
import plugintoolkit.composeapp.generated.resources.settings_locked_capability
import plugintoolkit.composeapp.generated.resources.settings_no_results
@@ -102,6 +109,11 @@ import plugintoolkit.composeapp.generated.resources.settings_search_placeholder
import org.wip.plugintoolkit.shared.components.verticalFadingEdges
+internal fun partitionSettings(
+ settings: Map
+): Pair