diff --git a/crates/local-deployment/src/lib.rs b/crates/local-deployment/src/lib.rs index 8b23ace942..348bf983bf 100644 --- a/crates/local-deployment/src/lib.rs +++ b/crates/local-deployment/src/lib.rs @@ -42,8 +42,21 @@ pub struct LocalDeployment { #[async_trait] impl Deployment for LocalDeployment { async fn new() -> Result { - let raw_config = load_config_from_file(&config_path()).await; - // Immediately save config, as it may have just been migrated + let mut raw_config = load_config_from_file(&config_path()).await; + + // Check if app version has changed and set release notes flag + { + let current_version = utils::version::APP_VERSION; + let stored_version = raw_config.last_app_version.as_deref(); + + if stored_version != Some(current_version) { + // Show release notes only if this is an upgrade (not first install) + raw_config.show_release_notes = stored_version.is_some(); + raw_config.last_app_version = Some(current_version.to_string()); + } + } + + // Always save config (may have been migrated or version updated) save_config_to_file(&raw_config, &config_path()).await?; let config = Arc::new(RwLock::new(raw_config)); diff --git a/crates/services/src/services/config/mod.rs b/crates/services/src/services/config/mod.rs index d2d8856c80..25ea907d75 100644 --- a/crates/services/src/services/config/mod.rs +++ b/crates/services/src/services/config/mod.rs @@ -14,13 +14,13 @@ pub enum ConfigError { ValidationError(String), } -pub type Config = versions::v4::Config; -pub type NotificationConfig = versions::v4::NotificationConfig; -pub type EditorConfig = versions::v4::EditorConfig; -pub type ThemeMode = versions::v4::ThemeMode; -pub type SoundFile = versions::v4::SoundFile; -pub type EditorType = versions::v4::EditorType; -pub type GitHubConfig = versions::v4::GitHubConfig; +pub type Config = versions::v5::Config; +pub type NotificationConfig = versions::v5::NotificationConfig; +pub type EditorConfig = versions::v5::EditorConfig; +pub type ThemeMode = versions::v5::ThemeMode; +pub type SoundFile = versions::v5::SoundFile; +pub type EditorType = versions::v5::EditorType; +pub type GitHubConfig = versions::v5::GitHubConfig; /// Will always return config, trying old schemas or eventually returning default pub async fn load_config_from_file(config_path: &PathBuf) -> Config { diff --git a/crates/services/src/services/config/versions/mod.rs b/crates/services/src/services/config/versions/mod.rs index 357b4e00ef..fb4978806d 100644 --- a/crates/services/src/services/config/versions/mod.rs +++ b/crates/services/src/services/config/versions/mod.rs @@ -2,3 +2,4 @@ pub(super) mod v1; pub(super) mod v2; pub(super) mod v3; pub(super) mod v4; +pub(super) mod v5; diff --git a/crates/services/src/services/config/versions/v5.rs b/crates/services/src/services/config/versions/v5.rs new file mode 100644 index 0000000000..0809b259fe --- /dev/null +++ b/crates/services/src/services/config/versions/v5.rs @@ -0,0 +1,97 @@ +use anyhow::Error; +use executors::profile::ProfileVariantLabel; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +pub use v4::{EditorConfig, EditorType, GitHubConfig, NotificationConfig, SoundFile, ThemeMode}; + +use crate::services::config::versions::v4; + +#[derive(Clone, Debug, Serialize, Deserialize, TS)] +pub struct Config { + pub config_version: String, + pub theme: ThemeMode, + pub profile: ProfileVariantLabel, + pub disclaimer_acknowledged: bool, + pub onboarding_acknowledged: bool, + pub github_login_acknowledged: bool, + pub telemetry_acknowledged: bool, + pub notifications: NotificationConfig, + pub editor: EditorConfig, + pub github: GitHubConfig, + pub analytics_enabled: Option, + pub workspace_dir: Option, + pub last_app_version: Option, + pub show_release_notes: bool, +} + +impl Config { + pub fn from_previous_version(raw_config: &str) -> Result { + let old_config = match serde_json::from_str::(raw_config) { + Ok(cfg) => cfg, + Err(e) => { + tracing::error!("❌ Failed to parse config: {}", e); + tracing::error!(" at line {}, column {}", e.line(), e.column()); + return Err(e.into()); + } + }; + + Ok(Self { + config_version: "v5".to_string(), + theme: old_config.theme, + profile: old_config.profile, + disclaimer_acknowledged: old_config.disclaimer_acknowledged, + onboarding_acknowledged: old_config.onboarding_acknowledged, + github_login_acknowledged: old_config.github_login_acknowledged, + telemetry_acknowledged: old_config.telemetry_acknowledged, + notifications: old_config.notifications, + editor: old_config.editor, + github: old_config.github, + analytics_enabled: old_config.analytics_enabled, + workspace_dir: old_config.workspace_dir, + last_app_version: None, + show_release_notes: false, + }) + } +} + +impl From for Config { + fn from(raw_config: String) -> Self { + if let Ok(config) = serde_json::from_str::(&raw_config) + && config.config_version == "v5" + { + return config; + } + + match Self::from_previous_version(&raw_config) { + Ok(config) => { + tracing::info!("Config upgraded to v5"); + config + } + Err(e) => { + tracing::warn!("Config migration failed: {}, using default", e); + Self::default() + } + } + } +} + +impl Default for Config { + fn default() -> Self { + Self { + config_version: "v5".to_string(), + theme: ThemeMode::System, + profile: ProfileVariantLabel::default("claude-code".to_string()), + disclaimer_acknowledged: false, + onboarding_acknowledged: false, + github_login_acknowledged: false, + telemetry_acknowledged: false, + notifications: NotificationConfig::default(), + editor: EditorConfig::default(), + github: GitHubConfig::default(), + analytics_enabled: None, + workspace_dir: None, + last_app_version: None, + show_release_notes: false, + } + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index cdf9f7891b..753bf5fe3d 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -14,6 +14,7 @@ pub mod sentry; pub mod shell; pub mod stream_lines; pub mod text; +pub mod version; /// Cache for WSL2 detection result static WSL2_CACHE: OnceLock = OnceLock::new(); diff --git a/crates/utils/src/version.rs b/crates/utils/src/version.rs new file mode 100644 index 0000000000..a8b686a592 --- /dev/null +++ b/crates/utils/src/version.rs @@ -0,0 +1,2 @@ +/// The current application version from Cargo.toml +pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6254f5747b..8b97f1c9f7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,6 +28,7 @@ import { configApi } from '@/lib/api'; import * as Sentry from '@sentry/react'; import { Loader } from '@/components/ui/loader'; import { GitHubLoginDialog } from '@/components/GitHubLoginDialog'; +import { ReleaseNotesDialog } from '@/components/ReleaseNotesDialog'; import { AppWithStyleOverride } from '@/utils/style-override'; import { WebviewContextMenu } from '@/vscode/ContextMenu'; @@ -45,6 +46,7 @@ function AppContent() { const [showOnboarding, setShowOnboarding] = useState(false); const [showPrivacyOptIn, setShowPrivacyOptIn] = useState(false); const [showGitHubLogin, setShowGitHubLogin] = useState(false); + const [showReleaseNotes, setShowReleaseNotes] = useState(false); const showNavbar = !location.pathname.endsWith('/full'); useEffect(() => { @@ -57,6 +59,8 @@ function AppContent() { setShowGitHubLogin(true); } else if (!config.telemetry_acknowledged) { setShowPrivacyOptIn(true); + } else if (config.show_release_notes) { + setShowReleaseNotes(true); } } } @@ -114,6 +118,9 @@ function AppContent() { try { await configApi.saveConfig(updatedConfig); setShowPrivacyOptIn(false); + if (updatedConfig.show_release_notes) { + setShowReleaseNotes(true); + } } catch (err) { console.error('Error saving config:', err); } @@ -139,10 +146,30 @@ function AppContent() { } finally { if (!config?.telemetry_acknowledged) { setShowPrivacyOptIn(true); + } else if (config?.show_release_notes) { + setShowReleaseNotes(true); } } }; + const handleReleaseNotesClose = async () => { + if (!config) return; + + const updatedConfig = { + ...config, + show_release_notes: false, + }; + + updateConfig(updatedConfig); + + try { + await configApi.saveConfig(updatedConfig); + setShowReleaseNotes(false); + } catch (err) { + console.error('Error saving config:', err); + } + }; + if (loading) { return (
@@ -174,6 +201,10 @@ function AppContent() { open={showPrivacyOptIn} onComplete={handlePrivacyOptInComplete} /> + void; +} + +const RELEASE_NOTES_URL = 'https://vibekanban.com/release-notes'; + +export function ReleaseNotesDialog({ open, onClose }: ReleaseNotesDialogProps) { + const [iframeError, setIframeError] = useState(false); + + const handleOpenInBrowser = () => { + window.open(RELEASE_NOTES_URL, '_blank'); + onClose(); + }; + + const handleIframeError = () => { + setIframeError(true); + }; + + return ( + !open && onClose()}> + + + + We've updated Vibe Kanban! Check out what's new... + + + + {iframeError ? ( +
+ +
+

+ Unable to load release notes +

+

+ We couldn't display the release notes in this window. Click + below to view them in your browser. +

+
+ +
+ ) : ( +