diff --git a/apps/mark/src-tauri/src/lib.rs b/apps/mark/src-tauri/src/lib.rs index 9c9f99cd..495c5074 100644 --- a/apps/mark/src-tauri/src/lib.rs +++ b/apps/mark/src-tauri/src/lib.rs @@ -2103,6 +2103,14 @@ pub fn run() { ..Default::default() }; + let settings_item = MenuItem::with_id( + handle, + "settings", + "Preferences…", + true, + Some("CmdOrCtrl+,"), + )?; + let app_menu = Submenu::with_items( handle, "Mark", @@ -2114,6 +2122,8 @@ pub fn run() { Some(about_metadata), )?, &PredefinedMenuItem::separator(handle)?, + &settings_item, + &PredefinedMenuItem::separator(handle)?, &PredefinedMenuItem::services(handle, None)?, &PredefinedMenuItem::separator(handle)?, &PredefinedMenuItem::hide(handle, None)?, @@ -2165,27 +2175,9 @@ pub fn run() { ], )?; - let doctor_item = - MenuItem::with_id(handle, "doctor", "Health Check…", true, None::<&str>)?; - - let help_menu = Submenu::with_id_and_items( - handle, - tauri::menu::HELP_SUBMENU_ID, - "Help", - true, - &[&doctor_item], - )?; - let menu = Menu::with_items( handle, - &[ - &app_menu, - &file_menu, - &edit_menu, - &view_menu, - &window_menu, - &help_menu, - ], + &[&app_menu, &file_menu, &edit_menu, &view_menu, &window_menu], )?; app.set_menu(menu)?; @@ -2302,10 +2294,10 @@ pub fn run() { Ok(()) }) .on_menu_event(|app, event| { - if event.id() == "doctor" { - // Emit an event to the frontend to open the doctor modal. - if let Err(e) = app.emit("menu:doctor", ()) { - log::warn!("Failed to emit menu:doctor event: {e}"); + if event.id() == "settings" { + // Emit an event to the frontend to open the settings page. + if let Err(e) = app.emit("menu:settings", ()) { + log::warn!("Failed to emit menu:settings event: {e}"); } } }) diff --git a/apps/mark/src/App.svelte b/apps/mark/src/App.svelte index c1239908..fc19659c 100644 --- a/apps/mark/src/App.svelte +++ b/apps/mark/src/App.svelte @@ -13,13 +13,12 @@ import ProjectHome from './lib/features/projects/ProjectHome.svelte'; import ProjectsList from './lib/features/projects/ProjectsList.svelte'; import SessionLauncher from './lib/features/sessions/SessionLauncher.svelte'; - import DoctorModal from './lib/features/doctor/DoctorModal.svelte'; - import ActionsPreferencesModal from './lib/features/settings/ActionsPreferencesModal.svelte'; + import SettingsPage from './lib/features/settings/SettingsPage.svelte'; import ToastHost from './lib/shared/ToastHost.svelte'; import { preferences, initPreferences } from './lib/features/settings/preferences.svelte'; import { refreshProviders } from './lib/features/agents/agent.svelte'; import { refreshSqAvailability } from './lib/features/settings/sq.svelte'; - import { navigation, initNavigation } from './lib/navigation.svelte'; + import { navigation, initNavigation, openSettings } from './lib/navigation.svelte'; import { projectStateStore } from './lib/stores/projectState.svelte'; import { prStateStore } from './lib/stores/prState.svelte'; import { sessionRegistry } from './lib/stores/sessionRegistry.svelte'; @@ -27,14 +26,11 @@ import type { StoreIncompatibility } from './lib/types'; let showSessionLab = $state(false); - let showDoctor = $state(false); - let showActionsPreferences = $state(false); - let unlistenDoctor: UnlistenFn | undefined; + let unlistenSettings: UnlistenFn | undefined; let unlistenSessionStatus: UnlistenFn | undefined; let storeIncompat = $state(null); let resetting = $state(false); let storeError = $state(null); - let onOpenActionsPreferences: (() => void) | null = null; // Konami code: ↑↑↓↓←→←→BA const konamiSequence = [ @@ -72,23 +68,19 @@ function handleGlobalShortcut(e: KeyboardEvent) { if (shouldIgnoreGlobalShortcut(e.target)) return; - if (e.key === ';') { + if ((e.metaKey || e.ctrlKey) && e.key === ',') { e.preventDefault(); - showActionsPreferences = true; + openSettings(); } } onMount(async () => { document.addEventListener('keydown', handleKonamiKey); document.addEventListener('keydown', handleGlobalShortcut); - onOpenActionsPreferences = () => { - showActionsPreferences = true; - }; - window.addEventListener('mark:open-actions-preferences', onOpenActionsPreferences); - - // Listen for the Help → Health Check… menu item. - unlistenDoctor = await listen('menu:doctor', () => { - showDoctor = true; + + // Listen for the app menu Preferences item. + unlistenSettings = await listen('menu:settings', () => { + openSettings(); }); // Listen for session status changes globally to handle spinner cleanup @@ -210,11 +202,7 @@ onDestroy(() => { document.removeEventListener('keydown', handleKonamiKey); document.removeEventListener('keydown', handleGlobalShortcut); - if (onOpenActionsPreferences) { - window.removeEventListener('mark:open-actions-preferences', onOpenActionsPreferences); - onOpenActionsPreferences = null; - } - unlistenDoctor?.(); + unlistenSettings?.(); unlistenSessionStatus?.(); }); @@ -293,6 +281,8 @@

{storeError}

+ {:else if navigation.activeView === 'settings'} + {:else if navigation.selectedProjectId} {:else} @@ -306,14 +296,6 @@ (showSessionLab = false)} /> {/if} - {#if showDoctor} - (showDoctor = false)} /> - {/if} - - {#if showActionsPreferences} - (showActionsPreferences = false)} /> - {/if} - {/if} diff --git a/apps/mark/src/lib/TopBar.svelte b/apps/mark/src/lib/TopBar.svelte index 3860f990..503d8816 100644 --- a/apps/mark/src/lib/TopBar.svelte +++ b/apps/mark/src/lib/TopBar.svelte @@ -9,6 +9,7 @@ import { Palette, PanelLeftClose, PanelLeftOpen, Plus, SlidersHorizontal } from 'lucide-svelte'; import { getCurrentWindow } from '@tauri-apps/api/window'; import ThemeSelectorModal from './features/settings/ThemeSelectorModal.svelte'; + import { navigation, openSettings } from './navigation.svelte'; import { hydrateProjectsSidebarState, projectsSidebarState, @@ -59,7 +60,10 @@ @@ -72,11 +76,7 @@ - diff --git a/apps/mark/src/lib/features/doctor/DoctorModal.svelte b/apps/mark/src/lib/features/doctor/DoctorModal.svelte index 41c1744f..5add1586 100644 --- a/apps/mark/src/lib/features/doctor/DoctorModal.svelte +++ b/apps/mark/src/lib/features/doctor/DoctorModal.svelte @@ -1,7 +1,7 @@ - - @@ -417,7 +49,7 @@ } .modal { - width: min(1100px, 94vw); + width: min(1160px, 94vw); max-height: 88vh; background: var(--bg-chrome); border: 1px solid var(--border-muted); @@ -438,15 +70,11 @@ .modal-header h2 { margin: 0; - display: flex; - align-items: center; - gap: 8px; font-size: var(--size-md); font-weight: 600; } - .close-btn, - .icon-btn { + .close-btn { display: inline-flex; align-items: center; justify-content: center; @@ -459,222 +87,15 @@ cursor: pointer; } - .close-btn:hover, - .icon-btn:hover { + .close-btn:hover { background: var(--bg-hover); color: var(--text-primary); } - .icon-btn.danger:hover { - color: var(--ui-danger); - } - .modal-body { - display: grid; - grid-template-columns: 260px 1fr; - min-height: 460px; - overflow: hidden; - } - - .sidebar { - border-right: 1px solid var(--border-subtle); - padding: 10px; - overflow-y: auto; - } - - .sidebar-title { - font-size: var(--size-xs); - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.06em; - margin: 4px 6px 10px; - } - - .context-list { - display: flex; - flex-direction: column; - gap: 4px; - } - - .context-item { - text-align: left; - padding: 8px 10px; - border: 1px solid transparent; - border-radius: 8px; - background: transparent; - color: var(--text-primary); - cursor: pointer; - font-size: var(--size-sm); - } - - .context-item:hover { - background: var(--bg-hover); - } - - .context-item.selected { - background: var(--bg-primary); - border-color: var(--border-muted); - } - - .loading-side, - .empty-side, - .empty-main, - .loading-state, - .empty-state { - color: var(--text-muted); - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - } - - .main-panel { + min-height: 0; + flex: 1; padding: 14px; - overflow-y: auto; - } - - .actions-header { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-bottom: 12px; - } - - .primary-btn, - .secondary-btn { - border: 1px solid var(--border-muted); - border-radius: 8px; - padding: 7px 10px; - display: inline-flex; - align-items: center; - gap: 6px; - font-size: var(--size-sm); - cursor: pointer; - } - - .primary-btn { - background: var(--ui-accent); - border-color: var(--ui-accent); - color: white; - } - - .secondary-btn { - background: var(--bg-primary); - color: var(--text-primary); - } - - .primary-btn:disabled, - .secondary-btn:disabled { - opacity: 0.6; - cursor: not-allowed; - } - - .actions-list { - display: flex; - flex-direction: column; - gap: 12px; - } - - .action-group { - border: 1px solid var(--border-subtle); - border-radius: 10px; overflow: hidden; } - - .group-header { - background: var(--bg-primary); - color: var(--text-muted); - font-size: var(--size-xs); - text-transform: uppercase; - letter-spacing: 0.06em; - padding: 8px 10px; - } - - .action-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 10px; - border-top: 1px solid var(--border-subtle); - } - - .action-main { - display: flex; - gap: 8px; - align-items: flex-start; - } - - .action-name { - font-size: var(--size-sm); - font-weight: 600; - } - - .action-command { - display: flex; - align-items: center; - gap: 4px; - color: var(--text-muted); - font-family: 'SF Mono', Menlo, Monaco, monospace; - font-size: var(--size-xs); - margin-top: 3px; - } - - .action-buttons { - display: flex; - gap: 4px; - } - - .editor { - border-top: 1px solid var(--border-subtle); - padding: 12px; - display: grid; - grid-template-columns: 1fr 1fr auto auto; - gap: 8px; - align-items: center; - } - - .editor input, - .editor select { - padding: 8px 10px; - border-radius: 8px; - border: 1px solid var(--border-muted); - background: var(--bg-primary); - color: var(--text-primary); - min-width: 0; - } - - .checkbox-row { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--text-muted); - font-size: var(--size-sm); - } - - .editor-buttons { - display: inline-flex; - gap: 8px; - justify-self: end; - } - - @media (max-width: 900px) { - .modal-body { - grid-template-columns: 1fr; - } - - .sidebar { - border-right: none; - border-bottom: 1px solid var(--border-subtle); - max-height: 160px; - } - - .editor { - grid-template-columns: 1fr; - } - - .editor-buttons { - justify-self: stretch; - } - } diff --git a/apps/mark/src/lib/features/settings/ActionsSettingsPanel.svelte b/apps/mark/src/lib/features/settings/ActionsSettingsPanel.svelte new file mode 100644 index 00000000..55b46ad3 --- /dev/null +++ b/apps/mark/src/lib/features/settings/ActionsSettingsPanel.svelte @@ -0,0 +1,642 @@ + + +
+
+

+ + Actions +

+

Configure per-repository action commands used across your projects.

+
+ +
+ + +
+ {#if !selectedContext} +
Select a repo context to configure actions
+ {:else} +
+ + +
+ + {#if loadingActions} +
+ + Loading... +
+ {:else if actions.length === 0} +
+ +

No actions configured

+

Click "Detect Actions" or add one manually

+
+ {:else} +
+ {#each Object.entries(groupedActions) as [type, typeActions]} + {#if typeActions.length > 0} +
+
{type}
+ {#each typeActions as action (action.id)} + {@const Icon = getActionIcon(action.actionType)} +
+
+ +
+
{action.name}
+
+ + {action.command} +
+
+
+
+ + +
+
+ {/each} +
+ {/if} + {/each} +
+ {/if} + {/if} +
+
+ + {#if editingAction} +
+ + + + +
+ + +
+
+ {/if} +
+ + diff --git a/apps/mark/src/lib/features/settings/DoctorSettingsPanel.svelte b/apps/mark/src/lib/features/settings/DoctorSettingsPanel.svelte new file mode 100644 index 00000000..68aff7ea --- /dev/null +++ b/apps/mark/src/lib/features/settings/DoctorSettingsPanel.svelte @@ -0,0 +1,195 @@ + + +
+
+
+

+ + Doctor +

+

Verify required tools and agent availability for Mark.

+
+ + +
+ +
+ {#if doctorState.loading} +
+ + Running checks... +
+ {:else if doctorState.report} +
+ +
+ {#each toolChecks as check (check.id)} + + {/each} +
+
+ + {#if agentChecks.length > 0} +
+ +
+ {#each agentChecks as check (check.id)} + + {/each} +
+
+ {/if} + {:else} +
No checks are available yet.
+ {/if} +
+
+ + diff --git a/apps/mark/src/lib/features/settings/SettingsPage.svelte b/apps/mark/src/lib/features/settings/SettingsPage.svelte new file mode 100644 index 00000000..8f7d14a5 --- /dev/null +++ b/apps/mark/src/lib/features/settings/SettingsPage.svelte @@ -0,0 +1,274 @@ + + +
+
+ +
+

Settings

+
+
+ +
+ + +
+ {#if navigation.settingsSection === 'actions'} + + {:else} + + {/if} +
+
+
+ + diff --git a/apps/mark/src/lib/navigation.svelte.ts b/apps/mark/src/lib/navigation.svelte.ts index f48b0901..ac9b7e5e 100644 --- a/apps/mark/src/lib/navigation.svelte.ts +++ b/apps/mark/src/lib/navigation.svelte.ts @@ -2,8 +2,10 @@ * Lightweight client-side navigation state. * * Controls which view is shown in the main content area: - * - `selectedProjectId === null` → ProjectsList (landing page) - * - `selectedProjectId === ` → ProjectHome filtered to that project + * - `activeView === 'settings'` → Settings page + * - `activeView === 'workspace'` + `selectedProjectId === null` → ProjectsList (landing page) + * - `activeView === 'workspace'` + `selectedProjectId === ` → ProjectHome filtered to that project + * - `settingsSection` selects which settings panel is shown * * The last viewed project is persisted so the user returns to it on relaunch. */ @@ -14,10 +16,18 @@ import { projectStateStore } from './stores/projectState.svelte'; const LAST_PROJECT_STORE_KEY = 'last-viewed-project'; +export type SettingsSection = 'actions' | 'doctor'; + export const navigation = $state({ + activeView: 'workspace' as 'workspace' | 'settings', selectedProjectId: null as string | null, + settingsSection: 'actions' as SettingsSection, }); +function showWorkspaceView(): void { + navigation.activeView = 'workspace'; +} + /** * Persist the current navigation target. * Saves `null` for the home screen or the project ID. @@ -55,6 +65,7 @@ export async function initNavigation(): Promise { /** Navigate to a specific project's detail view. */ export function selectProject(projectId: string): void { + showWorkspaceView(); navigation.selectedProjectId = projectId; persistLastProject(projectId); // Mark the project as read when navigating to it, but only if it's not already read @@ -65,6 +76,7 @@ export function selectProject(projectId: string): void { /** Navigate to a project and scroll to a specific branch card. */ export function selectProjectAndBranch(projectId: string, branchId: string): void { + showWorkspaceView(); const alreadyOnProject = navigation.selectedProjectId === projectId; navigation.selectedProjectId = projectId; persistLastProject(projectId); @@ -85,6 +97,18 @@ export function selectProjectAndBranch(projectId: string, branchId: string): voi /** Navigate back to the projects list (landing page). */ export function goHome(): void { + showWorkspaceView(); navigation.selectedProjectId = null; persistLastProject(null); } + +/** Show the dedicated settings view and select a settings section. */ +export function openSettings(section: SettingsSection = 'actions'): void { + navigation.settingsSection = section; + navigation.activeView = 'settings'; +} + +/** Return from settings to the workspace view (project or home). */ +export function closeSettings(): void { + showWorkspaceView(); +}