From 7ec5e8290139ca830d8c09ac3c5ad73f25ea17f2 Mon Sep 17 00:00:00 2001 From: Hai Phuc Nguyen <3423575+haiphucnguyen@users.noreply.github.com> Date: Mon, 6 Jul 2026 23:46:41 -0700 Subject: [PATCH] feat(core): enable runtime feature flag support - Centralizes configuration for feature flags in `io.askimo.core`. - Enables components like DiscoverView and NativeMenuBar to conditionally render UI elements based on feature toggles. - Allows phased rollout and controlled activation of new or experimental features without requiring a full codebase deployment. --- .../io/askimo/ui/discover/DiscoverView.kt | 171 ++++++---- .../io/askimo/ui/shell/NativeMenuBar.kt | 316 ++++++++---------- gradle.properties | 2 +- .../io/askimo/core/config/FeatureFlags.kt | 41 +++ 4 files changed, 297 insertions(+), 233 deletions(-) create mode 100644 shared/src/main/kotlin/io/askimo/core/config/FeatureFlags.kt diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/discover/DiscoverView.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/discover/DiscoverView.kt index a11c6c8a..5a085678 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/discover/DiscoverView.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/discover/DiscoverView.kt @@ -47,6 +47,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.text.font.FontWeight @@ -54,6 +55,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.askimo.core.AppConstants.DOMAIN import io.askimo.core.chat.domain.ChatSession +import io.askimo.core.config.FeatureFlags import io.askimo.core.user.domain.UserProfile import io.askimo.core.util.TimeUtil import io.askimo.ui.common.components.clickableCard @@ -193,34 +195,46 @@ private fun statCardsSection( onClick = onNavigateToSessions, modifier = Modifier.weight(1f), ) - statCard( - label = stringResource("discover.stat.projects"), - value = totalProjects?.toString() ?: "—", - icon = { Icon(Icons.Default.FolderOpen, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, - onClick = onNavigateToProjects, - modifier = Modifier.weight(1f), - ) - statCard( - label = stringResource("discover.stat.mcp"), - value = totalMcpServers.toString(), - icon = { Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, - onClick = onNavigateToMcpSettings, - modifier = Modifier.weight(1f), - ) - statCard( - label = stringResource("discover.stat.plans"), - value = totalPlans?.toString() ?: "—", - icon = { Icon(Icons.Default.PlayCircle, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, - onClick = onNavigateToPlans, - modifier = Modifier.weight(1f), - ) - statCard( - label = stringResource("discover.stat.skills"), - value = totalSkills?.toString() ?: "—", - icon = { Icon(Icons.Default.Extension, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, - onClick = onNavigateToSkills, - modifier = Modifier.weight(1f), - ) + + if (FeatureFlags.projectsEnabled) { + statCard( + label = stringResource("discover.stat.projects"), + value = totalProjects?.toString() ?: "—", + icon = { Icon(Icons.Default.FolderOpen, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, + onClick = onNavigateToProjects, + modifier = Modifier.weight(1f), + ) + } + + if (FeatureFlags.mcpIntegrationEnabled) { + statCard( + label = stringResource("discover.stat.mcp"), + value = totalMcpServers.toString(), + icon = { Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, + onClick = onNavigateToMcpSettings, + modifier = Modifier.weight(1f), + ) + } + + if (FeatureFlags.plansEnabled) { + statCard( + label = stringResource("discover.stat.plans"), + value = totalPlans?.toString() ?: "—", + icon = { Icon(Icons.Default.PlayCircle, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, + onClick = onNavigateToPlans, + modifier = Modifier.weight(1f), + ) + } + + if (FeatureFlags.skillsEnabled) { + statCard( + label = stringResource("discover.stat.skills"), + value = totalSkills?.toString() ?: "—", + icon = { Icon(Icons.Default.Extension, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, + onClick = onNavigateToSkills, + modifier = Modifier.weight(1f), + ) + } } } @@ -272,8 +286,65 @@ private fun statCard( } } +/** + * Data class for explore feature card configuration. + */ +private data class ExploreCardData( + val icon: ImageVector, + val titleKey: String, + val descKey: String, + val url: String, +) + @Composable private fun exploreFeaturesSection() { + // Build list of enabled explore cards based on feature flags + val enabledCards = buildList { + if (FeatureFlags.mcpIntegrationEnabled) { + add( + ExploreCardData( + icon = Icons.Default.Extension, + titleKey = "discover.explore.mcp.title", + descKey = "discover.explore.mcp.desc", + url = "https://$DOMAIN/docs/desktop/mcp-integration/", + ), + ) + } + if (FeatureFlags.ragEnabled) { + add( + ExploreCardData( + icon = Icons.AutoMirrored.Filled.LibraryBooks, + titleKey = "discover.explore.rag.title", + descKey = "discover.explore.rag.desc", + url = "https://$DOMAIN/docs/desktop/rag/", + ), + ) + } + if (FeatureFlags.plansEnabled) { + add( + ExploreCardData( + icon = Icons.Default.PlayCircle, + titleKey = "discover.explore.plans.title", + descKey = "discover.explore.plans.desc", + url = "https://$DOMAIN/docs/desktop/plans/", + ), + ) + } + if (FeatureFlags.skillsEnabled) { + add( + ExploreCardData( + icon = Icons.Default.Extension, + titleKey = "discover.explore.skills.title", + descKey = "discover.explore.skills.desc", + url = "https://$DOMAIN/docs/desktop/skills/", + ), + ) + } + } + + // Don't show section if no cards are enabled + if (enabledCards.isEmpty()) return + Column(verticalArrangement = Arrangement.spacedBy(Spacing.medium)) { Text( text = stringResource("discover.explore.title"), @@ -287,34 +358,22 @@ private fun exploreFeaturesSection() { .height(IntrinsicSize.Max), horizontalArrangement = Arrangement.spacedBy(Spacing.large), ) { - exploreCard( - icon = { Icon(Icons.Default.Extension, contentDescription = null, modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, - title = stringResource("discover.explore.mcp.title"), - description = stringResource("discover.explore.mcp.desc"), - url = "https://$DOMAIN/docs/desktop/mcp-integration/", - modifier = Modifier.weight(1f).fillMaxHeight(), - ) - exploreCard( - icon = { Icon(Icons.AutoMirrored.Filled.LibraryBooks, contentDescription = null, modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, - title = stringResource("discover.explore.rag.title"), - description = stringResource("discover.explore.rag.desc"), - url = "https://$DOMAIN/docs/desktop/rag/", - modifier = Modifier.weight(1f).fillMaxHeight(), - ) - exploreCard( - icon = { Icon(Icons.Default.PlayCircle, contentDescription = null, modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, - title = stringResource("discover.explore.plans.title"), - description = stringResource("discover.explore.plans.desc"), - url = "https://$DOMAIN/docs/desktop/plans/", - modifier = Modifier.weight(1f).fillMaxHeight(), - ) - exploreCard( - icon = { Icon(Icons.Default.Extension, contentDescription = null, modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, - title = stringResource("discover.explore.skills.title"), - description = stringResource("discover.explore.skills.desc"), - url = "https://$DOMAIN/docs/desktop/skills/", - modifier = Modifier.weight(1f).fillMaxHeight(), - ) + enabledCards.forEach { card -> + exploreCard( + icon = { + Icon( + card.icon, + contentDescription = null, + modifier = Modifier.size(22.dp), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + ) + }, + title = stringResource(card.titleKey), + description = stringResource(card.descKey), + url = card.url, + modifier = Modifier.weight(1f).fillMaxHeight(), + ) + } } } } diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/shell/NativeMenuBar.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/shell/NativeMenuBar.kt index 48e15cda..3333142b 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/shell/NativeMenuBar.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/shell/NativeMenuBar.kt @@ -7,6 +7,7 @@ package io.askimo.ui.shell import androidx.compose.ui.window.FrameWindowScope import io.askimo.core.AppConstants.DOMAIN import io.askimo.core.config.AppConfig +import io.askimo.core.config.FeatureFlags import io.askimo.core.i18n.LocalizationManager import io.askimo.core.util.AskimoHome import io.askimo.ui.common.theme.ThemeMode @@ -19,7 +20,6 @@ import java.awt.MenuBar import java.awt.MenuItem import java.awt.MenuShortcut import java.awt.Window -import java.awt.event.ActionListener import java.awt.event.KeyEvent import java.net.URI @@ -149,29 +149,24 @@ object NativeMenuBar { if (window is Frame) { val menuBar = MenuBar() - // File Menu val fileMenu = Menu(LocalizationManager.getString("menu.file")) val newChatItem = MenuItem( LocalizationManager.getString("chat.new"), MenuShortcut(KeyEvent.VK_N), ) - newChatItem.addActionListener( - ActionListener { - onNewChat() - }, - ) + newChatItem.addActionListener { + onNewChat() + } fileMenu.add(newChatItem) val newProjectItem = MenuItem( LocalizationManager.getString("menu.new.project"), MenuShortcut(KeyEvent.VK_N, true), // Shift+Ctrl+N (or Shift+Cmd+N on Mac) ) - newProjectItem.addActionListener( - ActionListener { - onNewProject() - }, - ) + newProjectItem.addActionListener { + onNewProject() + } fileMenu.add(newProjectItem) fileMenu.addSeparator() @@ -180,11 +175,9 @@ object NativeMenuBar { LocalizationManager.getString("menu.search.sessions"), MenuShortcut(KeyEvent.VK_F, true), // Shift+Ctrl+F (or Shift+Cmd+F on Mac) ) - searchSessionsItem.addActionListener( - ActionListener { - onSearchInSessions() - }, - ) + searchSessionsItem.addActionListener { + onSearchInSessions() + } fileMenu.add(searchSessionsItem) fileMenu.addSeparator() @@ -193,22 +186,18 @@ object NativeMenuBar { LocalizationManager.getString("menu.export.backup"), MenuShortcut(KeyEvent.VK_E, true), // Shift+Ctrl+E (or Shift+Cmd+E on Mac) ) - exportBackupItem.addActionListener( - ActionListener { - onExportBackup() - }, - ) + exportBackupItem.addActionListener { + onExportBackup() + } fileMenu.add(exportBackupItem) val importBackupItem = MenuItem( LocalizationManager.getString("menu.import.backup"), MenuShortcut(KeyEvent.VK_I, true), // Shift+Ctrl+I (or Shift+Cmd+I on Mac) ) - importBackupItem.addActionListener( - ActionListener { - onImportBackup() - }, - ) + importBackupItem.addActionListener { + onImportBackup() + } fileMenu.add(importBackupItem) fileMenu.addSeparator() @@ -216,21 +205,17 @@ object NativeMenuBar { val invalidateCachesItem = MenuItem( LocalizationManager.getString("menu.invalidate.caches"), ) - invalidateCachesItem.addActionListener( - ActionListener { - onInvalidateCaches() - }, - ) + invalidateCachesItem.addActionListener { + onInvalidateCaches() + } fileMenu.add(invalidateCachesItem) val clearPreferencesItem = MenuItem( LocalizationManager.getString("menu.clear.preferences"), ) - clearPreferencesItem.addActionListener( - ActionListener { - onClearPreferences() - }, - ) + clearPreferencesItem.addActionListener { + onClearPreferences() + } fileMenu.add(clearPreferencesItem) fileMenu.addSeparator() @@ -239,11 +224,9 @@ object NativeMenuBar { LocalizationManager.getString("settings.title"), MenuShortcut(KeyEvent.VK_COMMA), ) - settingsItem.addActionListener( - ActionListener { - onShowSettings() - }, - ) + settingsItem.addActionListener { + onShowSettings() + } fileMenu.add(settingsItem) menuBar.add(fileMenu) @@ -270,28 +253,22 @@ object NativeMenuBar { // Initialize labels updateThemeMenuItems() - systemThemeItem.addActionListener( - ActionListener { - ThemePreferences.setThemeMode(ThemeMode.SYSTEM) - updateThemeMenuItems() - }, - ) + systemThemeItem.addActionListener { + ThemePreferences.setThemeMode(ThemeMode.SYSTEM) + updateThemeMenuItems() + } appearanceMenu.add(systemThemeItem) - lightThemeItem.addActionListener( - ActionListener { - ThemePreferences.setThemeMode(ThemeMode.LIGHT) - updateThemeMenuItems() - }, - ) + lightThemeItem.addActionListener { + ThemePreferences.setThemeMode(ThemeMode.LIGHT) + updateThemeMenuItems() + } appearanceMenu.add(lightThemeItem) - darkThemeItem.addActionListener( - ActionListener { - ThemePreferences.setThemeMode(ThemeMode.DARK) - updateThemeMenuItems() - }, - ) + darkThemeItem.addActionListener { + ThemePreferences.setThemeMode(ThemeMode.DARK) + updateThemeMenuItems() + } appearanceMenu.add(darkThemeItem) viewMenu.add(appearanceMenu) @@ -304,41 +281,44 @@ object NativeMenuBar { LocalizationManager.getString("menu.view.discover"), MenuShortcut(KeyEvent.VK_D), ) - discoverItemGo.addActionListener(ActionListener { onNavigateToDiscover() }) + discoverItemGo.addActionListener { onNavigateToDiscover() } viewMenu.add(discoverItemGo) - // Plans toggle - val plansToggleItem = MenuItem("") - val updatePlansMenuItemFunc: (Boolean) -> Unit = { visible -> - plansToggleItem.label = (if (visible) "✓ " else " ") + - LocalizationManager.getString("menu.view.plans") - } - updatePlansMenuItem = updatePlansMenuItemFunc - updatePlansMenuItemFunc(isPlansVisible) - plansToggleItem.addActionListener(ActionListener { onTogglePlans?.invoke() }) - viewMenu.add(plansToggleItem) - - // Skills toggle - val skillsToggleItem = MenuItem("") - val updateSkillsMenuItemFunc: (Boolean) -> Unit = { visible -> - skillsToggleItem.label = (if (visible) "✓ " else " ") + - LocalizationManager.getString("menu.view.skills") - } - updateSkillsMenuItem = updateSkillsMenuItemFunc - updateSkillsMenuItemFunc(isSkillsVisible) - skillsToggleItem.addActionListener(ActionListener { onToggleSkills?.invoke() }) - viewMenu.add(skillsToggleItem) - - // Projects toggle - val projectsToggleItem = MenuItem("") - val updateProjectsMenuItemFunc: (Boolean) -> Unit = { visible -> - projectsToggleItem.label = (if (visible) "✓ " else " ") + - LocalizationManager.getString("menu.view.projects") - } - updateProjectsMenuItem = updateProjectsMenuItemFunc - updateProjectsMenuItemFunc(isProjectsVisible) - projectsToggleItem.addActionListener(ActionListener { onToggleProjects?.invoke() }) - viewMenu.add(projectsToggleItem) + if (FeatureFlags.plansEnabled) { + val plansToggleItem = MenuItem("") + val updatePlansMenuItemFunc: (Boolean) -> Unit = { visible -> + plansToggleItem.label = (if (visible) "✓ " else " ") + + LocalizationManager.getString("menu.view.plans") + } + updatePlansMenuItem = updatePlansMenuItemFunc + updatePlansMenuItemFunc(isPlansVisible) + plansToggleItem.addActionListener { onTogglePlans?.invoke() } + viewMenu.add(plansToggleItem) + } + + if (FeatureFlags.skillsEnabled) { + val skillsToggleItem = MenuItem("") + val updateSkillsMenuItemFunc: (Boolean) -> Unit = { visible -> + skillsToggleItem.label = (if (visible) "✓ " else " ") + + LocalizationManager.getString("menu.view.skills") + } + updateSkillsMenuItem = updateSkillsMenuItemFunc + updateSkillsMenuItemFunc(isSkillsVisible) + skillsToggleItem.addActionListener { onToggleSkills?.invoke() } + viewMenu.add(skillsToggleItem) + } + + if (FeatureFlags.projectsEnabled) { + val projectsToggleItem = MenuItem("") + val updateProjectsMenuItemFunc: (Boolean) -> Unit = { visible -> + projectsToggleItem.label = (if (visible) "✓ " else " ") + + LocalizationManager.getString("menu.view.projects") + } + updateProjectsMenuItem = updateProjectsMenuItemFunc + updateProjectsMenuItemFunc(isProjectsVisible) + projectsToggleItem.addActionListener { onToggleProjects?.invoke() } + viewMenu.add(projectsToggleItem) + } viewMenu.addSeparator() @@ -356,22 +336,18 @@ object NativeMenuBar { updateSidebarMenuItemFunc(true) - toggleSidebarItem.addActionListener( - ActionListener { - onToggleSidebar() - }, - ) + toggleSidebarItem.addActionListener { + onToggleSidebar() + } viewMenu.add(toggleSidebarItem) val fullScreenItem = MenuItem( LocalizationManager.getString("menu.view.fullscreen"), MenuShortcut(KeyEvent.VK_F, true), // Ctrl+Cmd+F on Mac, Ctrl+F on others ) - fullScreenItem.addActionListener( - ActionListener { - onEnterFullScreen() - }, - ) + fullScreenItem.addActionListener { + onEnterFullScreen() + } viewMenu.add(fullScreenItem) menuBar.add(viewMenu) @@ -383,11 +359,9 @@ object NativeMenuBar { LocalizationManager.getString("menu.terminal.new"), MenuShortcut(KeyEvent.VK_T, true), // Shift+Ctrl+T (or Shift+Cmd+T on Mac) ) - newTerminalItem.addActionListener( - ActionListener { - onOpenTerminal() - }, - ) + newTerminalItem.addActionListener { + onOpenTerminal() + } terminalMenu.add(newTerminalItem) menuBar.add(terminalMenu) @@ -396,62 +370,64 @@ object NativeMenuBar { val helpMenu = Menu(menuLabel("menu.help")) val docsItem = MenuItem(menuLabel("menu.documentation")) - docsItem.addActionListener( - ActionListener { - runCatching { if (Desktop.isDesktopSupported()) Desktop.getDesktop().browse(URI("https://$DOMAIN/docs/")) } - }, - ) + docsItem.addActionListener { + runCatching { + if (Desktop.isDesktopSupported()) Desktop.getDesktop().browse(URI("https://$DOMAIN/docs/")) + } + } helpMenu.add(docsItem) // Getting started val gettingStartedItem = MenuItem(menuLabel("menu.help.gettingstarted")) - gettingStartedItem.addActionListener( - ActionListener { - onShowGettingStarted() - }, - ) + gettingStartedItem.addActionListener { + onShowGettingStarted() + } helpMenu.add(gettingStartedItem) // Share Feedback — moved here from the footer status bar for better discoverability val shareFeedbackItem = MenuItem(menuLabel("system.share.feedback")) - shareFeedbackItem.addActionListener( - ActionListener { - runCatching { - if (Desktop.isDesktopSupported()) { - Desktop.getDesktop().browse(URI("https://$DOMAIN/contact/")) - } + shareFeedbackItem.addActionListener { + runCatching { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().browse(URI("https://$DOMAIN/contact/")) } - }, - ) + } + } helpMenu.add(shareFeedbackItem) helpMenu.addSeparator() // Release Notes val releaseNotesItem = MenuItem(menuLabel("menu.help.release.notes")) - releaseNotesItem.addActionListener( - ActionListener { - runCatching { if (Desktop.isDesktopSupported()) Desktop.getDesktop().browse(URI("https://$DOMAIN/docs/changelogs/")) } - }, - ) + releaseNotesItem.addActionListener { + runCatching { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop() + .browse(URI("https://$DOMAIN/docs/changelogs/")) + } + } + } helpMenu.add(releaseNotesItem) // Star on GitHub val starGitHubItem = MenuItem(menuLabel("menu.help.star.github")) - starGitHubItem.addActionListener( - ActionListener { - runCatching { if (Desktop.isDesktopSupported()) Desktop.getDesktop().browse(URI("https://github.com/askimo-ai/askimo")) } - }, - ) + starGitHubItem.addActionListener { + runCatching { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop() + .browse(URI("https://github.com/askimo-ai/askimo")) + } + } + } helpMenu.add(starGitHubItem) // Join Discord Community val discordItem = MenuItem(menuLabel("menu.help.discord")) - discordItem.addActionListener( - ActionListener { - runCatching { if (Desktop.isDesktopSupported()) Desktop.getDesktop().browse(URI("https://discord.gg/eXSBR4fNmm")) } - }, - ) + discordItem.addActionListener { + runCatching { + if (Desktop.isDesktopSupported()) Desktop.getDesktop().browse(URI("https://discord.gg/eXSBR4fNmm")) + } + } helpMenu.add(discordItem) // Share Askimo submenu @@ -459,7 +435,7 @@ object NativeMenuBar { ShareTarget.entries.forEach { target -> val item = MenuItem(ShareUtils.labelFor(target)) - item.addActionListener(ActionListener { ShareUtils.share(target) }) + item.addActionListener { ShareUtils.share(target) } shareMenu.add(item) } @@ -469,65 +445,53 @@ object NativeMenuBar { // Check for Updates val checkUpdatesItem = MenuItem(menuLabel("menu.help.check.updates")) - checkUpdatesItem.addActionListener( - ActionListener { - onCheckForUpdates() - }, - ) + checkUpdatesItem.addActionListener { + onCheckForUpdates() + } helpMenu.add(checkUpdatesItem) // Event Log (Developer Tools) val eventLogItem = MenuItem(menuLabel("menu.eventlog")) - eventLogItem.addActionListener( - ActionListener { - onShowEventLog() - }, - ) + eventLogItem.addActionListener { + onShowEventLog() + } helpMenu.add(eventLogItem) // Open Model Capabilities File val modelCapabilitiesItem = MenuItem(menuLabel("menu.help.model.capabilities")) - modelCapabilitiesItem.addActionListener( - ActionListener { - runCatching { - val file = AskimoHome.base().resolve("model-capabilities-cache.json").toFile() - if (file.exists() && Desktop.isDesktopSupported()) { - Desktop.getDesktop().open(file) - } + modelCapabilitiesItem.addActionListener { + runCatching { + val file = AskimoHome.base().resolve("model-capabilities-cache.json").toFile() + if (file.exists() && Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(file) } - }, - ) + } + } helpMenu.add(modelCapabilitiesItem) // System Diagnostics — live CPU/memory and session telemetry val systemDiagnosticsItem = MenuItem(menuLabel("menu.help.diagnostics")) - systemDiagnosticsItem.addActionListener( - ActionListener { - onShowSystemDiagnostics() - }, - ) + systemDiagnosticsItem.addActionListener { + onShowSystemDiagnostics() + } helpMenu.add(systemDiagnosticsItem) // Clear Account Preferences (Developer Tools — only shown when developer mode is active) val devConfig = AppConfig.developer if (devConfig.enabled && devConfig.active) { val clearAccountPrefsItem = MenuItem(menuLabel("menu.dev.clear.account.preferences")) - clearAccountPrefsItem.addActionListener( - ActionListener { - onClearAccountPreferences() - }, - ) + clearAccountPrefsItem.addActionListener { + onClearAccountPreferences() + } helpMenu.add(clearAccountPrefsItem) } helpMenu.addSeparator() val aboutItem = MenuItem(menuLabel("menu.about")) - aboutItem.addActionListener( - ActionListener { - onShowAbout() - }, - ) + aboutItem.addActionListener { + onShowAbout() + } helpMenu.add(aboutItem) menuBar.add(helpMenu) diff --git a/gradle.properties b/gradle.properties index c4116167..ac5045c1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ # Project metadata projectGroup=io.askimo -projectVersion=1.4.11 +projectVersion=1.4.12 # About information author=Askimo diff --git a/shared/src/main/kotlin/io/askimo/core/config/FeatureFlags.kt b/shared/src/main/kotlin/io/askimo/core/config/FeatureFlags.kt new file mode 100644 index 00000000..a132dc73 --- /dev/null +++ b/shared/src/main/kotlin/io/askimo/core/config/FeatureFlags.kt @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: AGPLv3 + * + * Copyright (c) 2026 Askimo + */ +package io.askimo.core.config + +/** + * Feature flags for controlling feature availability across Askimo editions. + * + * Initialize at app startup via [FeatureFlags.initialize]. + */ +data class FeatureFlagsConfig( + val plansEnabled: Boolean = true, + val skillsEnabled: Boolean = true, + val projectsEnabled: Boolean = true, + val discoverEnabled: Boolean = true, + val mcpIntegrationEnabled: Boolean = true, + val ragEnabled: Boolean = true, +) + +/** + * Singleton accessor for feature flags. + */ +object FeatureFlags { + @Volatile + private var config: FeatureFlagsConfig = FeatureFlagsConfig() + + /** + * Initialize feature flags at app startup. + * Call once from Main.kt before any UI is rendered. + */ + fun initialize(config: FeatureFlagsConfig) { + this.config = config + } + + val plansEnabled: Boolean get() = config.plansEnabled + val skillsEnabled: Boolean get() = config.skillsEnabled + val projectsEnabled: Boolean get() = config.projectsEnabled + val mcpIntegrationEnabled: Boolean get() = config.mcpIntegrationEnabled + val ragEnabled: Boolean get() = config.ragEnabled +}