diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index f9d7b8d7..e07d8284 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2847,7 +2847,7 @@ dependencies = [ [[package]] name = "openchamber-desktop" -version = "1.2.6" +version = "1.2.7" dependencies = [ "anyhow", "axum", diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index 6dac9e5b..b445df94 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -90,6 +90,8 @@ const MENU_ITEM_REQUEST_FEATURE_ID: &str = "openchamber_request_feature"; // App menu #[cfg(target_os = "macos")] +const MENU_ITEM_ABOUT_ID: &str = "openchamber_about"; +#[cfg(target_os = "macos")] const MENU_ITEM_SETTINGS_ID: &str = "openchamber_settings"; #[cfg(target_os = "macos")] const MENU_ITEM_COMMAND_PALETTE_ID: &str = "openchamber_command_palette"; @@ -329,17 +331,17 @@ fn prevent_app_nap() { #[cfg(target_os = "macos")] fn build_macos_menu(app: &tauri::AppHandle) -> tauri::Result> { - use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID}; + use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID}; let pkg_info = app.package_info(); - let config = app.config(); - let about_metadata = AboutMetadata { - name: Some(pkg_info.name.clone()), - version: Some(pkg_info.version.to_string()), - copyright: config.bundle.copyright.clone(), - authors: config.bundle.publisher.clone().map(|p| vec![p]), - ..Default::default() - }; + + let about = MenuItem::with_id( + app, + MENU_ITEM_ABOUT_ID, + format!("About {}", pkg_info.name), + true, + None::<&str>, + )?; let check_for_updates = MenuItem::with_id( app, @@ -378,7 +380,7 @@ fn build_macos_menu(app: &tauri::AppHandle) -> tauri::Resu let worktree_creator = MenuItem::with_id( app, MENU_ITEM_WORKTREE_CREATOR_ID, - "New Worktree…", + "New Worktree", true, Some("Ctrl+Shift+N"), )?; @@ -386,7 +388,7 @@ fn build_macos_menu(app: &tauri::AppHandle) -> tauri::Resu let change_workspace = MenuItem::with_id( app, MENU_ITEM_CHANGE_WORKSPACE_ID, - "Change Workspace…", + "Change Workspace", true, None::<&str>, )?; @@ -476,7 +478,7 @@ fn build_macos_menu(app: &tauri::AppHandle) -> tauri::Resu let report_bug = MenuItem::with_id( app, MENU_ITEM_REPORT_BUG_ID, - "Report a Bug…", + "Report a Bug", true, None::<&str>, )?; @@ -484,7 +486,7 @@ fn build_macos_menu(app: &tauri::AppHandle) -> tauri::Resu let request_feature = MenuItem::with_id( app, MENU_ITEM_REQUEST_FEATURE_ID, - "Request a Feature…", + "Request a Feature", true, None::<&str>, )?; @@ -531,7 +533,7 @@ fn build_macos_menu(app: &tauri::AppHandle) -> tauri::Resu pkg_info.name.clone(), true, &[ - &PredefinedMenuItem::about(app, None, Some(about_metadata))?, + &about, &check_for_updates, &PredefinedMenuItem::separator(app)?, &settings, @@ -870,6 +872,11 @@ fn main() { } // App menu actions + if event_id == MENU_ITEM_ABOUT_ID { + let _ = app.emit("openchamber:menu-action", "about"); + return; + } + if event_id == MENU_ITEM_SETTINGS_ID { let _ = app.emit("openchamber:menu-action", "settings"); return; diff --git a/packages/desktop/vite.config.ts b/packages/desktop/vite.config.ts index 16557984..93cd137c 100644 --- a/packages/desktop/vite.config.ts +++ b/packages/desktop/vite.config.ts @@ -2,9 +2,11 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { readFileSync } from 'node:fs'; import { themeStoragePlugin } from '../../vite-theme-plugin'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8')); export default defineConfig({ root: path.resolve(__dirname, '.'), @@ -23,6 +25,7 @@ export default defineConfig({ 'process.version': JSON.stringify('v20.0.0'), 'process.versions': JSON.stringify({}), global: 'globalThis', + __APP_VERSION__: JSON.stringify(packageJson.version), }, optimizeDeps: { include: ['@opencode-ai/sdk'], diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 024298a8..15832a84 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -18,12 +18,24 @@ import { opencodeClient } from '@/lib/opencode/client'; import { useFontPreferences } from '@/hooks/useFontPreferences'; import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions'; import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay'; +import { AboutDialog } from '@/components/ui/AboutDialog'; import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen'; import { isCliAvailable } from '@/lib/desktop'; +import { useUIStore } from '@/stores/useUIStore'; import type { RuntimeAPIs } from '@/lib/api/types'; +const AboutDialogWrapper: React.FC = () => { + const { isAboutDialogOpen, setAboutDialogOpen } = useUIStore(); + return ( + + ); +}; + type AppProps = { apis: RuntimeAPIs; }; @@ -200,6 +212,7 @@ function App({ apis }: AppProps) { + {showMemoryDebug && ( setShowMemoryDebug(false)} /> )} diff --git a/packages/ui/src/components/layout/Sidebar.tsx b/packages/ui/src/components/layout/Sidebar.tsx index fcda76b0..b1de20c1 100644 --- a/packages/ui/src/components/layout/Sidebar.tsx +++ b/packages/ui/src/components/layout/Sidebar.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { RiDownloadLine, RiSettings3Line } from '@remixicon/react'; +import { RiDownloadLine, RiInformationLine, RiSettings3Line } from '@remixicon/react'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { ErrorBoundary } from '../ui/ErrorBoundary'; import { useUIStore } from '@/stores/useUIStore'; import { useUpdateCheck } from '@/hooks/useUpdateCheck'; import { UpdateDialog } from '../ui/UpdateDialog'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; export const SIDEBAR_CONTENT_WIDTH = 264; const SIDEBAR_MIN_WIDTH = 200; @@ -20,7 +21,7 @@ interface SidebarProps { } export const Sidebar: React.FC = ({ isOpen, isMobile, children }) => { - const { sidebarWidth, setSidebarWidth, setSettingsDialogOpen } = useUIStore(); + const { sidebarWidth, setSidebarWidth, setSettingsDialogOpen, setAboutDialogOpen } = useUIStore(); const [isResizing, setIsResizing] = React.useState(false); const startXRef = React.useRef(0); const startWidthRef = React.useRef(sidebarWidth || SIDEBAR_CONTENT_WIDTH); @@ -229,7 +230,7 @@ export const Sidebar: React.FC = ({ isOpen, isMobile, children }) Settings - {(available || downloaded) && ( + {(available || downloaded) ? ( + ) : !isDesktopApp && ( + + + + + About OpenChamber + )} diff --git a/packages/ui/src/components/ui/AboutDialog.tsx b/packages/ui/src/components/ui/AboutDialog.tsx new file mode 100644 index 00000000..aa4fe9db --- /dev/null +++ b/packages/ui/src/components/ui/AboutDialog.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { + Dialog, + DialogContent, +} from '@/components/ui/dialog'; +import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; +import { RiGithubFill, RiTwitterXFill } from '@remixicon/react'; + +declare const __APP_VERSION__: string | undefined; + +interface AboutDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export const AboutDialog: React.FC = ({ + open, + onOpenChange, +}) => { + const [version, setVersion] = React.useState(null); + + React.useEffect(() => { + if (!open) return; + + const isDesktop = typeof window !== 'undefined' && !!window.opencodeDesktop; + + if (isDesktop) { + const fetchVersion = async () => { + try { + const { getVersion } = await import('@tauri-apps/api/app'); + const v = await getVersion(); + setVersion(v); + } catch { + setVersion(typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : null); + } + }; + fetchVersion(); + } else { + setVersion(typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : null); + } + }, [open]); + + const displayVersion = version; + + return ( + + +
+ + +
+

OpenChamber

+ {displayVersion && ( +

+ Version {displayVersion} +

+ )} +
+ +

+ A beautiful interface for OpenCode AI coding agent +

+ + + +

+ Made with care for developers +

+
+
+
+ ); +}; diff --git a/packages/ui/src/hooks/useMenuActions.ts b/packages/ui/src/hooks/useMenuActions.ts index d5957173..8213f028 100644 --- a/packages/ui/src/hooks/useMenuActions.ts +++ b/packages/ui/src/hooks/useMenuActions.ts @@ -12,6 +12,7 @@ import { isDesktopRuntime } from '@/lib/desktop'; const MENU_ACTION_EVENT = 'openchamber:menu-action'; type MenuAction = + | 'about' | 'settings' | 'command-palette' | 'new-session' @@ -39,6 +40,7 @@ export const useMenuActions = ( setSessionCreateDialogOpen, setActiveMainTab, setSettingsDialogOpen, + setAboutDialogOpen, } = useUIStore(); const { agents } = useConfigStore(); const { setDirectory } = useDirectoryStore(); @@ -72,6 +74,10 @@ export const useMenuActions = ( const action = (event as CustomEvent).detail; switch (action) { + case 'about': + setAboutDialogOpen(true); + break; + case 'settings': setSettingsDialogOpen(true); break; @@ -185,6 +191,7 @@ export const useMenuActions = ( setSessionCreateDialogOpen, setActiveMainTab, setSettingsDialogOpen, + setAboutDialogOpen, setThemeMode, agents, onToggleMemoryDebug, diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 5bcca392..efdd4cf9 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -26,6 +26,7 @@ interface UIStore { isMobile: boolean; isCommandPaletteOpen: boolean; isHelpDialogOpen: boolean; + isAboutDialogOpen: boolean; isSessionCreateDialogOpen: boolean; isSettingsDialogOpen: boolean; sidebarSection: SidebarSection; @@ -58,6 +59,7 @@ interface UIStore { setCommandPaletteOpen: (open: boolean) => void; toggleHelpDialog: () => void; setHelpDialogOpen: (open: boolean) => void; + setAboutDialogOpen: (open: boolean) => void; setSessionCreateDialogOpen: (open: boolean) => void; setSettingsDialogOpen: (open: boolean) => void; applyTheme: () => void; @@ -93,6 +95,7 @@ export const useUIStore = create()( isMobile: false, isCommandPaletteOpen: false, isHelpDialogOpen: false, + isAboutDialogOpen: false, isSessionCreateDialogOpen: false, isSettingsDialogOpen: false, sidebarSection: 'sessions', @@ -192,6 +195,10 @@ export const useUIStore = create()( set({ isHelpDialogOpen: open }); }, + setAboutDialogOpen: (open) => { + set({ isAboutDialogOpen: open }); + }, + setSessionCreateDialogOpen: (open) => { set({ isSessionCreateDialogOpen: open }); }, diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index 85b40f79..e3d3c749 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -2,9 +2,11 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { readFileSync } from 'node:fs'; import { themeStoragePlugin } from '../../vite-theme-plugin'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8')); export default defineConfig({ root: path.resolve(__dirname, '.'), @@ -23,6 +25,7 @@ export default defineConfig({ define: { 'process.env': {}, global: 'globalThis', + __APP_VERSION__: JSON.stringify(packageJson.version), }, optimizeDeps: { include: ['@opencode-ai/sdk'],