diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..95c6e6d3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,58 @@ +name: Bug report +description: Report a reproducible problem +title: "bug: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug. + + Please include description/screenshots if possible. + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: What did you expect, what happened instead? + placeholder: Steps + expected vs actual + validations: + required: true + + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: Numbered steps help a lot + placeholder: | + 1. + 2. + 3. + validations: + required: true + + - type: dropdown + id: runtime + attributes: + label: Runtime + multiple: true + options: + - Desktop MacOS app + - Mobile (Web/PWA) + - Desktop Web + - VS Code extension + validations: + required: true + + - type: input + id: version + attributes: + label: OpenChamber version + placeholder: "e.g. 1.2.3" + + - type: textarea + id: logs + attributes: + label: Logs / screenshots + description: Paste relevant logs or attach screenshots + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..20d6a92e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: GitHub Discussions + url: https://github.com/btriapitsyn/openchamber/discussions + about: Ask questions and share ideas diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..3aa3bad7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +name: Feature request +description: Suggest an improvement or new capability +title: "feat: " +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + Thanks for proposing an idea. + + - type: textarea + id: problem + attributes: + label: Problem / use case + description: What are you trying to do? + placeholder: "As a user, I want..." + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: What would you like to see happen? + placeholder: UI, behavior, config, etc. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + placeholder: "Other approaches you've tried/considered" + + - type: textarea + id: additional + attributes: + label: Additional context + description: Links, screenshots, mockups, etc. diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index fea4ca88..f2ddcafe 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.2" +version = "1.2.3" dependencies = [ "anyhow", "axum", diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index 875f1a19..0a24b794 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -79,6 +79,18 @@ const MODELS_DEV_API_URL: &str = "https://models.dev/api.json"; const MODELS_METADATA_CACHE_TTL: Duration = Duration::from_secs(5 * 60); const MODELS_METADATA_REQUEST_TIMEOUT: Duration = Duration::from_secs(8); +const CHECK_FOR_UPDATES_EVENT: &str = "openchamber:check-for-updates"; + +#[cfg(target_os = "macos")] +const MENU_ITEM_CHECK_FOR_UPDATES_ID: &str = "openchamber_check_for_updates"; +#[cfg(target_os = "macos")] +const MENU_ITEM_REPORT_BUG_ID: &str = "openchamber_report_bug"; +#[cfg(target_os = "macos")] +const MENU_ITEM_REQUEST_FEATURE_ID: &str = "openchamber_request_feature"; + +const GITHUB_BUG_REPORT_URL: &str = "https://github.com/btriapitsyn/openchamber/issues/new?template=bug_report.yml"; +const GITHUB_FEATURE_REQUEST_URL: &str = "https://github.com/btriapitsyn/openchamber/issues/new?template=feature_request.yml"; + #[derive(Clone)] pub(crate) struct DesktopRuntime { server_port: u16, @@ -277,6 +289,116 @@ fn prevent_app_nap() { info!("[macos] App Nap prevention enabled via objc2"); } +#[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}; + + 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 check_for_updates = MenuItem::with_id( + app, + MENU_ITEM_CHECK_FOR_UPDATES_ID, + "Check for Updates…", + true, + None::<&str>, + )?; + + let report_bug = MenuItem::with_id( + app, + MENU_ITEM_REPORT_BUG_ID, + "Report a Bug…", + true, + None::<&str>, + )?; + + let request_feature = MenuItem::with_id( + app, + MENU_ITEM_REQUEST_FEATURE_ID, + "Request a Feature…", + true, + None::<&str>, + )?; + + let window_menu = Submenu::with_id_and_items( + app, + WINDOW_SUBMENU_ID, + "Window", + true, + &[ + &PredefinedMenuItem::minimize(app, None)?, + &PredefinedMenuItem::maximize(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::close_window(app, None)?, + ], + )?; + + let help_menu = Submenu::with_id_and_items( + app, + HELP_SUBMENU_ID, + "Help", + true, + &[&report_bug, &request_feature], + )?; + + Menu::with_items( + app, + &[ + &Submenu::with_items( + app, + pkg_info.name.clone(), + true, + &[ + &PredefinedMenuItem::about(app, None, Some(about_metadata))?, + &check_for_updates, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::services(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::hide(app, None)?, + &PredefinedMenuItem::hide_others(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::quit(app, None)?, + ], + )?, + &Submenu::with_items( + app, + "File", + true, + &[&PredefinedMenuItem::close_window(app, None)?], + )?, + &Submenu::with_items( + app, + "Edit", + true, + &[ + &PredefinedMenuItem::undo(app, None)?, + &PredefinedMenuItem::redo(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::cut(app, None)?, + &PredefinedMenuItem::copy(app, None)?, + &PredefinedMenuItem::paste(app, None)?, + &PredefinedMenuItem::select_all(app, None)?, + ], + )?, + &Submenu::with_items( + app, + "View", + true, + &[&PredefinedMenuItem::fullscreen(app, None)?], + )?, + &window_menu, + &help_menu, + ], + ) +} + fn main() { let mut log_builder = tauri_plugin_log::Builder::default() .level(log::LevelFilter::Info) @@ -299,6 +421,16 @@ fn main() { .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) .plugin(log_builder.build()) + .menu(|app| { + #[cfg(target_os = "macos")] + { + return build_macos_menu(app); + } + #[cfg(not(target_os = "macos"))] + { + return tauri::menu::Menu::default(app); + } + }) .setup(|app| { #[cfg(target_os = "macos")] prevent_app_nap(); @@ -418,6 +550,33 @@ fn main() { fetch_desktop_logs, desktop_notify, ]) + .on_menu_event(|app, event| { + #[cfg(target_os = "macos")] + { + if event.id() == MENU_ITEM_CHECK_FOR_UPDATES_ID { + let _ = app.emit(CHECK_FOR_UPDATES_EVENT, ()); + return; + } + + if event.id() == MENU_ITEM_REPORT_BUG_ID { + use tauri_plugin_shell::ShellExt; + #[allow(deprecated)] + { + let _ = app.shell().open(GITHUB_BUG_REPORT_URL, None); + } + return; + } + + if event.id() == MENU_ITEM_REQUEST_FEATURE_ID { + use tauri_plugin_shell::ShellExt; + #[allow(deprecated)] + { + let _ = app.shell().open(GITHUB_FEATURE_REQUEST_URL, None); + } + return; + } + } + }) .on_window_event(|window, event| { let window_state_manager = window.state::().inner().clone(); diff --git a/packages/desktop/src/main.tsx b/packages/desktop/src/main.tsx index fc35632d..1c4b8251 100644 --- a/packages/desktop/src/main.tsx +++ b/packages/desktop/src/main.tsx @@ -45,6 +45,8 @@ declare global { } } +const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates'; + const cleanupFunctions: Array<() => void | Promise> = []; try { @@ -55,6 +57,11 @@ try { }); cleanupFunctions.push(() => activityUnlisten()); + const updateCheckUnlisten = await listen(CHECK_FOR_UPDATES_EVENT, () => { + window.dispatchEvent(new CustomEvent(CHECK_FOR_UPDATES_EVENT)); + }); + cleanupFunctions.push(() => updateCheckUnlisten()); + requestInitialNotificationPermission().catch(err => { console.error('[main] Failed to request notification permission:', err); }); diff --git a/packages/ui/src/components/layout/Sidebar.tsx b/packages/ui/src/components/layout/Sidebar.tsx index dd787aea..1387120b 100644 --- a/packages/ui/src/components/layout/Sidebar.tsx +++ b/packages/ui/src/components/layout/Sidebar.tsx @@ -10,6 +10,7 @@ export const SIDEBAR_CONTENT_WIDTH = 264; const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MAX_WIDTH = 500; const MAC_TITLEBAR_SAFE_AREA = 40; +const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates'; interface SidebarProps { isOpen: boolean; @@ -25,6 +26,10 @@ export const Sidebar: React.FC = ({ isOpen, isMobile, children }) const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false); const update = useUpdateCheck(); + const pendingMenuUpdateCheckRef = React.useRef(false); + + const checkForUpdates = update.checkForUpdates; + const { available, downloaded, checking } = update; const [isDesktopApp, setIsDesktopApp] = React.useState(() => { if (typeof window === 'undefined') { @@ -48,6 +53,41 @@ export const Sidebar: React.FC = ({ isOpen, isMobile, children }) setIsDesktopApp(detected); }, []); + React.useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + const handleMenuUpdateCheck = () => { + const hasDesktopApi = + typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined'; + if (!hasDesktopApi) { + return; + } + pendingMenuUpdateCheckRef.current = true; + void checkForUpdates(); + }; + + window.addEventListener(CHECK_FOR_UPDATES_EVENT, handleMenuUpdateCheck as EventListener); + return () => { + window.removeEventListener(CHECK_FOR_UPDATES_EVENT, handleMenuUpdateCheck as EventListener); + }; + }, [checkForUpdates]); + + React.useEffect(() => { + if (!pendingMenuUpdateCheckRef.current) { + return; + } + if (checking) { + return; + } + + if (available || downloaded) { + setUpdateDialogOpen(true); + } + pendingMenuUpdateCheckRef.current = false; + }, [available, downloaded, checking]); + React.useEffect(() => { if (isMobile || !isResizing) { return; @@ -184,7 +224,7 @@ export const Sidebar: React.FC = ({ isOpen, isMobile, children }) Settings - {(update.available || update.downloaded) && ( + {(available || downloaded) && (