feat: add about dialog with custom information

This commit is contained in:
Bohdan Triapitsyn
2025-12-19 18:59:08 +02:00
parent ae5ff9c049
commit bddb174bd3
9 changed files with 168 additions and 18 deletions
+1 -1
View File
@@ -2847,7 +2847,7 @@ dependencies = [
[[package]] [[package]]
name = "openchamber-desktop" name = "openchamber-desktop"
version = "1.2.6" version = "1.2.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
+21 -14
View File
@@ -90,6 +90,8 @@ const MENU_ITEM_REQUEST_FEATURE_ID: &str = "openchamber_request_feature";
// App menu // App menu
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const MENU_ITEM_ABOUT_ID: &str = "openchamber_about";
#[cfg(target_os = "macos")]
const MENU_ITEM_SETTINGS_ID: &str = "openchamber_settings"; const MENU_ITEM_SETTINGS_ID: &str = "openchamber_settings";
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const MENU_ITEM_COMMAND_PALETTE_ID: &str = "openchamber_command_palette"; const MENU_ITEM_COMMAND_PALETTE_ID: &str = "openchamber_command_palette";
@@ -329,17 +331,17 @@ fn prevent_app_nap() {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<tauri::menu::Menu<R>> { fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<tauri::menu::Menu<R>> {
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 pkg_info = app.package_info();
let config = app.config();
let about_metadata = AboutMetadata { let about = MenuItem::with_id(
name: Some(pkg_info.name.clone()), app,
version: Some(pkg_info.version.to_string()), MENU_ITEM_ABOUT_ID,
copyright: config.bundle.copyright.clone(), format!("About {}", pkg_info.name),
authors: config.bundle.publisher.clone().map(|p| vec![p]), true,
..Default::default() None::<&str>,
}; )?;
let check_for_updates = MenuItem::with_id( let check_for_updates = MenuItem::with_id(
app, app,
@@ -378,7 +380,7 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
let worktree_creator = MenuItem::with_id( let worktree_creator = MenuItem::with_id(
app, app,
MENU_ITEM_WORKTREE_CREATOR_ID, MENU_ITEM_WORKTREE_CREATOR_ID,
"New Worktree…", "New Worktree",
true, true,
Some("Ctrl+Shift+N"), Some("Ctrl+Shift+N"),
)?; )?;
@@ -386,7 +388,7 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
let change_workspace = MenuItem::with_id( let change_workspace = MenuItem::with_id(
app, app,
MENU_ITEM_CHANGE_WORKSPACE_ID, MENU_ITEM_CHANGE_WORKSPACE_ID,
"Change Workspace…", "Change Workspace",
true, true,
None::<&str>, None::<&str>,
)?; )?;
@@ -476,7 +478,7 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
let report_bug = MenuItem::with_id( let report_bug = MenuItem::with_id(
app, app,
MENU_ITEM_REPORT_BUG_ID, MENU_ITEM_REPORT_BUG_ID,
"Report a Bug…", "Report a Bug",
true, true,
None::<&str>, None::<&str>,
)?; )?;
@@ -484,7 +486,7 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
let request_feature = MenuItem::with_id( let request_feature = MenuItem::with_id(
app, app,
MENU_ITEM_REQUEST_FEATURE_ID, MENU_ITEM_REQUEST_FEATURE_ID,
"Request a Feature…", "Request a Feature",
true, true,
None::<&str>, None::<&str>,
)?; )?;
@@ -531,7 +533,7 @@ fn build_macos_menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Resu
pkg_info.name.clone(), pkg_info.name.clone(),
true, true,
&[ &[
&PredefinedMenuItem::about(app, None, Some(about_metadata))?, &about,
&check_for_updates, &check_for_updates,
&PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::separator(app)?,
&settings, &settings,
@@ -870,6 +872,11 @@ fn main() {
} }
// App menu actions // 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 { if event_id == MENU_ITEM_SETTINGS_ID {
let _ = app.emit("openchamber:menu-action", "settings"); let _ = app.emit("openchamber:menu-action", "settings");
return; return;
+3
View File
@@ -2,9 +2,11 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import { themeStoragePlugin } from '../../vite-theme-plugin'; import { themeStoragePlugin } from '../../vite-theme-plugin';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8'));
export default defineConfig({ export default defineConfig({
root: path.resolve(__dirname, '.'), root: path.resolve(__dirname, '.'),
@@ -23,6 +25,7 @@ export default defineConfig({
'process.version': JSON.stringify('v20.0.0'), 'process.version': JSON.stringify('v20.0.0'),
'process.versions': JSON.stringify({}), 'process.versions': JSON.stringify({}),
global: 'globalThis', global: 'globalThis',
__APP_VERSION__: JSON.stringify(packageJson.version),
}, },
optimizeDeps: { optimizeDeps: {
include: ['@opencode-ai/sdk'], include: ['@opencode-ai/sdk'],
+13
View File
@@ -18,12 +18,24 @@ import { opencodeClient } from '@/lib/opencode/client';
import { useFontPreferences } from '@/hooks/useFontPreferences'; import { useFontPreferences } from '@/hooks/useFontPreferences';
import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions'; 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 { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay';
import { AboutDialog } from '@/components/ui/AboutDialog';
import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen'; import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen';
import { isCliAvailable } from '@/lib/desktop'; import { isCliAvailable } from '@/lib/desktop';
import { useUIStore } from '@/stores/useUIStore';
import type { RuntimeAPIs } from '@/lib/api/types'; import type { RuntimeAPIs } from '@/lib/api/types';
const AboutDialogWrapper: React.FC = () => {
const { isAboutDialogOpen, setAboutDialogOpen } = useUIStore();
return (
<AboutDialog
open={isAboutDialogOpen}
onOpenChange={setAboutDialogOpen}
/>
);
};
type AppProps = { type AppProps = {
apis: RuntimeAPIs; apis: RuntimeAPIs;
}; };
@@ -200,6 +212,7 @@ function App({ apis }: AppProps) {
<MainLayout /> <MainLayout />
<Toaster /> <Toaster />
<ConfigUpdateOverlay /> <ConfigUpdateOverlay />
<AboutDialogWrapper />
{showMemoryDebug && ( {showMemoryDebug && (
<MemoryDebugPanel onClose={() => setShowMemoryDebug(false)} /> <MemoryDebugPanel onClose={() => setShowMemoryDebug(false)} />
)} )}
+21 -3
View File
@@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { RiDownloadLine, RiSettings3Line } from '@remixicon/react'; import { RiDownloadLine, RiInformationLine, RiSettings3Line } from '@remixicon/react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ErrorBoundary } from '../ui/ErrorBoundary'; import { ErrorBoundary } from '../ui/ErrorBoundary';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useUpdateCheck } from '@/hooks/useUpdateCheck'; import { useUpdateCheck } from '@/hooks/useUpdateCheck';
import { UpdateDialog } from '../ui/UpdateDialog'; import { UpdateDialog } from '../ui/UpdateDialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
export const SIDEBAR_CONTENT_WIDTH = 264; export const SIDEBAR_CONTENT_WIDTH = 264;
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
@@ -20,7 +21,7 @@ interface SidebarProps {
} }
export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children }) => { export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children }) => {
const { sidebarWidth, setSidebarWidth, setSettingsDialogOpen } = useUIStore(); const { sidebarWidth, setSidebarWidth, setSettingsDialogOpen, setAboutDialogOpen } = useUIStore();
const [isResizing, setIsResizing] = React.useState(false); const [isResizing, setIsResizing] = React.useState(false);
const startXRef = React.useRef(0); const startXRef = React.useRef(0);
const startWidthRef = React.useRef(sidebarWidth || SIDEBAR_CONTENT_WIDTH); const startWidthRef = React.useRef(sidebarWidth || SIDEBAR_CONTENT_WIDTH);
@@ -229,7 +230,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
<RiSettings3Line className="h-4 w-4" /> <RiSettings3Line className="h-4 w-4" />
<span>Settings</span> <span>Settings</span>
</button> </button>
{(available || downloaded) && ( {(available || downloaded) ? (
<button <button
onClick={() => setUpdateDialogOpen(true)} onClick={() => setUpdateDialogOpen(true)}
className={cn( className={cn(
@@ -243,6 +244,23 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
<RiDownloadLine className="h-3.5 w-3.5" /> <RiDownloadLine className="h-3.5 w-3.5" />
<span>Update</span> <span>Update</span>
</button> </button>
) : !isDesktopApp && (
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setAboutDialogOpen(true)}
className={cn(
'flex items-center justify-center rounded-md p-1.5',
'text-muted-foreground',
'hover:text-foreground hover:bg-muted/50',
'transition-colors'
)}
>
<RiInformationLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top">About OpenChamber</TooltipContent>
</Tooltip>
)} )}
</div> </div>
</div> </div>
@@ -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<AboutDialogProps> = ({
open,
onOpenChange,
}) => {
const [version, setVersion] = React.useState<string | null>(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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-xs p-6">
<div className="flex flex-col items-center text-center space-y-4">
<OpenChamberLogo width={64} height={64} />
<div className="space-y-1">
<h2 className="text-lg font-semibold">OpenChamber</h2>
{displayVersion && (
<p className="typography-meta text-muted-foreground">
Version {displayVersion}
</p>
)}
</div>
<p className="typography-meta text-muted-foreground">
A beautiful interface for OpenCode AI coding agent
</p>
<div className="flex items-center gap-4 pt-2">
<a
href="https://github.com/btriapitsyn/openchamber"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors"
>
<RiGithubFill className="h-4 w-4" />
<span>GitHub</span>
</a>
<a
href="https://x.com/btriapitsyn"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors"
>
<RiTwitterXFill className="h-4 w-4" />
<span>@btriapitsyn</span>
</a>
</div>
<p className="typography-meta text-muted-foreground/60 pt-2">
Made with care for developers
</p>
</div>
</DialogContent>
</Dialog>
);
};
+7
View File
@@ -12,6 +12,7 @@ import { isDesktopRuntime } from '@/lib/desktop';
const MENU_ACTION_EVENT = 'openchamber:menu-action'; const MENU_ACTION_EVENT = 'openchamber:menu-action';
type MenuAction = type MenuAction =
| 'about'
| 'settings' | 'settings'
| 'command-palette' | 'command-palette'
| 'new-session' | 'new-session'
@@ -39,6 +40,7 @@ export const useMenuActions = (
setSessionCreateDialogOpen, setSessionCreateDialogOpen,
setActiveMainTab, setActiveMainTab,
setSettingsDialogOpen, setSettingsDialogOpen,
setAboutDialogOpen,
} = useUIStore(); } = useUIStore();
const { agents } = useConfigStore(); const { agents } = useConfigStore();
const { setDirectory } = useDirectoryStore(); const { setDirectory } = useDirectoryStore();
@@ -72,6 +74,10 @@ export const useMenuActions = (
const action = (event as CustomEvent<MenuAction>).detail; const action = (event as CustomEvent<MenuAction>).detail;
switch (action) { switch (action) {
case 'about':
setAboutDialogOpen(true);
break;
case 'settings': case 'settings':
setSettingsDialogOpen(true); setSettingsDialogOpen(true);
break; break;
@@ -185,6 +191,7 @@ export const useMenuActions = (
setSessionCreateDialogOpen, setSessionCreateDialogOpen,
setActiveMainTab, setActiveMainTab,
setSettingsDialogOpen, setSettingsDialogOpen,
setAboutDialogOpen,
setThemeMode, setThemeMode,
agents, agents,
onToggleMemoryDebug, onToggleMemoryDebug,
+7
View File
@@ -26,6 +26,7 @@ interface UIStore {
isMobile: boolean; isMobile: boolean;
isCommandPaletteOpen: boolean; isCommandPaletteOpen: boolean;
isHelpDialogOpen: boolean; isHelpDialogOpen: boolean;
isAboutDialogOpen: boolean;
isSessionCreateDialogOpen: boolean; isSessionCreateDialogOpen: boolean;
isSettingsDialogOpen: boolean; isSettingsDialogOpen: boolean;
sidebarSection: SidebarSection; sidebarSection: SidebarSection;
@@ -58,6 +59,7 @@ interface UIStore {
setCommandPaletteOpen: (open: boolean) => void; setCommandPaletteOpen: (open: boolean) => void;
toggleHelpDialog: () => void; toggleHelpDialog: () => void;
setHelpDialogOpen: (open: boolean) => void; setHelpDialogOpen: (open: boolean) => void;
setAboutDialogOpen: (open: boolean) => void;
setSessionCreateDialogOpen: (open: boolean) => void; setSessionCreateDialogOpen: (open: boolean) => void;
setSettingsDialogOpen: (open: boolean) => void; setSettingsDialogOpen: (open: boolean) => void;
applyTheme: () => void; applyTheme: () => void;
@@ -93,6 +95,7 @@ export const useUIStore = create<UIStore>()(
isMobile: false, isMobile: false,
isCommandPaletteOpen: false, isCommandPaletteOpen: false,
isHelpDialogOpen: false, isHelpDialogOpen: false,
isAboutDialogOpen: false,
isSessionCreateDialogOpen: false, isSessionCreateDialogOpen: false,
isSettingsDialogOpen: false, isSettingsDialogOpen: false,
sidebarSection: 'sessions', sidebarSection: 'sessions',
@@ -192,6 +195,10 @@ export const useUIStore = create<UIStore>()(
set({ isHelpDialogOpen: open }); set({ isHelpDialogOpen: open });
}, },
setAboutDialogOpen: (open) => {
set({ isAboutDialogOpen: open });
},
setSessionCreateDialogOpen: (open) => { setSessionCreateDialogOpen: (open) => {
set({ isSessionCreateDialogOpen: open }); set({ isSessionCreateDialogOpen: open });
}, },
+3
View File
@@ -2,9 +2,11 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import { themeStoragePlugin } from '../../vite-theme-plugin'; import { themeStoragePlugin } from '../../vite-theme-plugin';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8'));
export default defineConfig({ export default defineConfig({
root: path.resolve(__dirname, '.'), root: path.resolve(__dirname, '.'),
@@ -23,6 +25,7 @@ export default defineConfig({
define: { define: {
'process.env': {}, 'process.env': {},
global: 'globalThis', global: 'globalThis',
__APP_VERSION__: JSON.stringify(packageJson.version),
}, },
optimizeDeps: { optimizeDeps: {
include: ['@opencode-ai/sdk'], include: ['@opencode-ai/sdk'],