From d0d34670c927320862be2e3025b03e56d6080055 Mon Sep 17 00:00:00 2001 From: Wes Date: Mon, 23 Feb 2026 15:14:48 -0700 Subject: [PATCH 1/5] feat(mark): add full-page settings with Cmd+, shortcut --- apps/mark/src-tauri/src/lib.rs | 15 + apps/mark/src/App.svelte | 30 +- apps/mark/src/lib/TopBar.svelte | 12 +- .../settings/ActionsPreferencesModal.svelte | 599 +--------------- .../settings/ActionsSettingsPanel.svelte | 642 ++++++++++++++++++ .../lib/features/settings/SettingsPage.svelte | 160 +++++ apps/mark/src/lib/navigation.svelte.ts | 23 +- 7 files changed, 866 insertions(+), 615 deletions(-) create mode 100644 apps/mark/src/lib/features/settings/ActionsSettingsPanel.svelte create mode 100644 apps/mark/src/lib/features/settings/SettingsPage.svelte diff --git a/apps/mark/src-tauri/src/lib.rs b/apps/mark/src-tauri/src/lib.rs index 9c9f99cd..4eaa38c7 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)?, @@ -2307,6 +2317,11 @@ pub fn run() { if let Err(e) = app.emit("menu:doctor", ()) { log::warn!("Failed to emit menu:doctor event: {e}"); } + } else 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}"); + } } }) .invoke_handler(tauri::generate_handler![ diff --git a/apps/mark/src/App.svelte b/apps/mark/src/App.svelte index c1239908..b27be576 100644 --- a/apps/mark/src/App.svelte +++ b/apps/mark/src/App.svelte @@ -14,12 +14,12 @@ 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'; @@ -28,13 +28,12 @@ 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,24 +71,24 @@ 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 // This must be at the App level so it works regardless of which view the user is on @@ -210,11 +209,8 @@ 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 +289,8 @@

{storeError}

+ {:else if navigation.activeView === 'settings'} + {:else if navigation.selectedProjectId} {:else} @@ -310,10 +308,6 @@ (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..dacea314 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/settings/ActionsPreferencesModal.svelte b/apps/mark/src/lib/features/settings/ActionsPreferencesModal.svelte index ab16f793..8c24a21e 100644 --- a/apps/mark/src/lib/features/settings/ActionsPreferencesModal.svelte +++ b/apps/mark/src/lib/features/settings/ActionsPreferencesModal.svelte @@ -1,25 +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/SettingsPage.svelte b/apps/mark/src/lib/features/settings/SettingsPage.svelte new file mode 100644 index 00000000..4481897b --- /dev/null +++ b/apps/mark/src/lib/features/settings/SettingsPage.svelte @@ -0,0 +1,160 @@ + + +
+
+ +
+

Settings

+

Manage workspace preferences and action automation.

+
+
+ +
+ + +
+ +
+
+
+ + diff --git a/apps/mark/src/lib/navigation.svelte.ts b/apps/mark/src/lib/navigation.svelte.ts index f48b0901..836a6b72 100644 --- a/apps/mark/src/lib/navigation.svelte.ts +++ b/apps/mark/src/lib/navigation.svelte.ts @@ -2,8 +2,9 @@ * 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 * * The last viewed project is persisted so the user returns to it on relaunch. */ @@ -15,9 +16,14 @@ import { projectStateStore } from './stores/projectState.svelte'; const LAST_PROJECT_STORE_KEY = 'last-viewed-project'; export const navigation = $state({ + activeView: 'workspace' as 'workspace' | 'settings', selectedProjectId: null as string | null, }); +function showWorkspaceView(): void { + navigation.activeView = 'workspace'; +} + /** * Persist the current navigation target. * Saves `null` for the home screen or the project ID. @@ -55,6 +61,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 +72,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 +93,17 @@ 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 while preserving current project context. */ +export function openSettings(): void { + navigation.activeView = 'settings'; +} + +/** Return from settings to the workspace view (project or home). */ +export function closeSettings(): void { + showWorkspaceView(); +} From 49a08731c4f4b9a001ba23a50ab54cf04c4440ea Mon Sep 17 00:00:00 2001 From: Wes Date: Mon, 23 Feb 2026 15:39:03 -0700 Subject: [PATCH 2/5] style(mark): soften settings back button and remove subtitle --- .../lib/features/settings/SettingsPage.svelte | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/mark/src/lib/features/settings/SettingsPage.svelte b/apps/mark/src/lib/features/settings/SettingsPage.svelte index 4481897b..8cc75e55 100644 --- a/apps/mark/src/lib/features/settings/SettingsPage.svelte +++ b/apps/mark/src/lib/features/settings/SettingsPage.svelte @@ -18,7 +18,6 @@

Settings

-

Manage workspace preferences and action automation.

@@ -59,20 +58,27 @@ } .back-btn { - border: 1px solid var(--border-muted); + border: 1px solid transparent; border-radius: 8px; - background: var(--bg-primary); - color: var(--text-primary); + background: transparent; + color: var(--text-muted); display: inline-flex; align-items: center; gap: 6px; - padding: 6px 10px; + padding: 6px 9px; cursor: pointer; flex-shrink: 0; + font-size: var(--size-sm); + transition: + color 0.12s ease, + background-color 0.12s ease, + border-color 0.12s ease; } .back-btn:hover { + color: var(--text-primary); background: var(--bg-hover); + border-color: color-mix(in srgb, var(--border-subtle) 60%, transparent); } .header-text { @@ -85,12 +91,6 @@ line-height: 1.2; } - .header-text p { - margin: 3px 0 0; - color: var(--text-muted); - font-size: var(--size-sm); - } - .settings-body { flex: 1; min-height: 0; From d330287f39ce1122519b1310a1a0dbe510bf4dfc Mon Sep 17 00:00:00 2001 From: Wes Date: Mon, 23 Feb 2026 15:55:23 -0700 Subject: [PATCH 3/5] feat(mark): move doctor into settings and soften nav --- apps/mark/src-tauri/src/lib.rs | 2 +- apps/mark/src/App.svelte | 10 +- apps/mark/src/lib/TopBar.svelte | 2 +- .../src/lib/features/doctor/doctor.svelte.ts | 2 +- .../settings/DoctorSettingsPanel.svelte | 195 ++++++++++++++++++ .../lib/features/settings/SettingsPage.svelte | 36 ++-- apps/mark/src/lib/navigation.svelte.ts | 9 +- 7 files changed, 231 insertions(+), 25 deletions(-) create mode 100644 apps/mark/src/lib/features/settings/DoctorSettingsPanel.svelte diff --git a/apps/mark/src-tauri/src/lib.rs b/apps/mark/src-tauri/src/lib.rs index 4eaa38c7..63bf849b 100644 --- a/apps/mark/src-tauri/src/lib.rs +++ b/apps/mark/src-tauri/src/lib.rs @@ -2313,7 +2313,7 @@ pub fn run() { }) .on_menu_event(|app, event| { if event.id() == "doctor" { - // Emit an event to the frontend to open the doctor modal. + // Emit an event to the frontend to open settings on the Doctor section. if let Err(e) = app.emit("menu:doctor", ()) { log::warn!("Failed to emit menu:doctor event: {e}"); } diff --git a/apps/mark/src/App.svelte b/apps/mark/src/App.svelte index b27be576..61bbc5bb 100644 --- a/apps/mark/src/App.svelte +++ b/apps/mark/src/App.svelte @@ -13,7 +13,6 @@ 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 SettingsPage from './lib/features/settings/SettingsPage.svelte'; import ToastHost from './lib/shared/ToastHost.svelte'; import { preferences, initPreferences } from './lib/features/settings/preferences.svelte'; @@ -27,7 +26,6 @@ import type { StoreIncompatibility } from './lib/types'; let showSessionLab = $state(false); - let showDoctor = $state(false); let unlistenDoctor: UnlistenFn | undefined; let unlistenSettings: UnlistenFn | undefined; let unlistenSessionStatus: UnlistenFn | undefined; @@ -81,9 +79,9 @@ document.addEventListener('keydown', handleKonamiKey); document.addEventListener('keydown', handleGlobalShortcut); - // Listen for the Help → Health Check… menu item. + // Listen for the Help → Health Check... menu item. unlistenDoctor = await listen('menu:doctor', () => { - showDoctor = true; + openSettings('doctor'); }); // Listen for the app menu Preferences item. unlistenSettings = await listen('menu:settings', () => { @@ -304,10 +302,6 @@ (showSessionLab = false)} /> {/if} - {#if showDoctor} - (showDoctor = false)} /> - {/if} - {/if} diff --git a/apps/mark/src/lib/TopBar.svelte b/apps/mark/src/lib/TopBar.svelte index dacea314..503d8816 100644 --- a/apps/mark/src/lib/TopBar.svelte +++ b/apps/mark/src/lib/TopBar.svelte @@ -76,7 +76,7 @@ - diff --git a/apps/mark/src/lib/features/doctor/doctor.svelte.ts b/apps/mark/src/lib/features/doctor/doctor.svelte.ts index b972a93e..456dc748 100644 --- a/apps/mark/src/lib/features/doctor/doctor.svelte.ts +++ b/apps/mark/src/lib/features/doctor/doctor.svelte.ts @@ -1,5 +1,5 @@ /** - * doctor.svelte.ts — Reactive state for the system health-check modal. + * doctor.svelte.ts — Reactive state for the system health-check UI. * * Exposes `doctorState` (report + loading flag) and an action helper * that calls the Tauri `run_doctor` command. 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 index 8cc75e55..e5cf4aaf 100644 --- a/apps/mark/src/lib/features/settings/SettingsPage.svelte +++ b/apps/mark/src/lib/features/settings/SettingsPage.svelte @@ -1,9 +1,8 @@