Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/mark/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"core:window:allow-close",
"clipboard-manager:allow-write-text",
"window-state:default",
"store:default"
"store:default",
"core:window:allow-set-badge-count"
]
}
14 changes: 14 additions & 0 deletions apps/mark/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,20 @@
// Cache the BLOX_ENV value once at startup (process-level, never changes).
initBloxEnv();

// Restore persisted unread state and sync the dock badge.
try {
await projectStateStore.initFromStore();
Comment on lines +290 to +292

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Initialize unread state before pruning deleted projects

initNavigation() now calls projectStateStore.pruneDeletedProjects(...), but this startup block restores unread IDs only afterwards, so prune runs against an empty in-memory map and cannot remove stale persisted unread IDs. In the current order (App.svelte calls initNavigation() before projectStateStore.initFromStore()), deleted projects can still be restored as unread on restart and inflate the dock badge count; loading unread state first (or pruning the persisted IDs directly) avoids that mismatch.

Useful? React with 👍 / 👎.

// If we restored into a project that was unread, mark it as read now
if (
navigation.selectedProjectId &&
projectStateStore.isUnread(navigation.selectedProjectId)
) {
projectStateStore.markAsRead(navigation.selectedProjectId);
}
} catch (e) {
console.error('Failed to restore unread state:', e);
}

try {
await initializeShortcutBindings();
} catch (e) {
Expand Down
6 changes: 4 additions & 2 deletions apps/mark/src/lib/features/layout/navigation.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ export async function initNavigation(): Promise<void> {
// Validate the project still exists before navigating to it
try {
const projects = await commands.listProjects();
const exists = projects.some((p) => p.id === lastProjectId);
if (exists) {
const existingIds = new Set(projects.map((p) => p.id));
if (existingIds.has(lastProjectId)) {
navigation.selectedProjectId = lastProjectId;
} else {
// Project was deleted — clear the stale value
await setStoreValue(LAST_PROJECT_STORE_KEY, null);
}
// Remove unread entries for projects that no longer exist
await projectStateStore.pruneDeletedProjects(existingIds);
} catch {
// If we can't list projects (e.g. store error), stay on home
console.warn('[Navigation] Could not verify last project, falling back to home');
Expand Down
80 changes: 80 additions & 0 deletions apps/mark/src/lib/stores/projectState.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
* - The user navigates to that project
*
* Running sessions are tracked to show spinners on project cards
* Unread state is persisted to disk and the macOS dock badge is kept in sync.
*
* Note: Session-to-project lookups are now delegated to the unified sessionRegistry
*/

import { getCurrentWindow } from '@tauri-apps/api/window';
import { getStoreValue, setStoreValue } from '../shared/persistentStore';
import { sessionRegistry, type SessionType } from './sessionRegistry.svelte';

const UNREAD_PROJECTS_STORE_KEY = 'unread-projects';

interface ProjectState {
unread: boolean;
runningSessions: Set<string>; // Set of session IDs currently running in this project
Expand Down Expand Up @@ -84,6 +89,7 @@ class ProjectStateStore {
const state = this.getOrCreateState(projectId);
state.unread = false;
this.version++; // Trigger reactivity
this.persistAndBadge();
}

/**
Expand All @@ -94,6 +100,7 @@ class ProjectStateStore {
const state = this.getOrCreateState(projectId);
state.unread = true;
this.version++; // Trigger reactivity
this.persistAndBadge();
}

/**
Expand Down Expand Up @@ -190,6 +197,79 @@ class ProjectStateStore {
}
return types;
}

/**
* Get the number of projects currently in the unread state.
*/
getUnreadCount(): number {
this.version;
let count = 0;
for (const state of this.states.values()) {
if (state.unread) count++;
}
return count;
}

/**
* Persist the current set of unread project IDs to disk and update the dock badge.
*/
private persistAndBadge(): void {
const ids: string[] = [];
for (const [id, state] of this.states) {
if (state.unread) ids.push(id);
}
setStoreValue(UNREAD_PROJECTS_STORE_KEY, ids);
getCurrentWindow()
.setBadgeCount(ids.length || undefined)
.catch(() => {
// setBadgeCount may be unsupported on some platforms — ignore
});
}

/**
* Restore unread state from the persistent store and sync the badge.
* Must be called after initPersistentStore().
*/
async initFromStore(): Promise<void> {
const ids = await getStoreValue<string[]>(UNREAD_PROJECTS_STORE_KEY);
if (ids && ids.length > 0) {
for (const id of ids) {
const state = this.getOrCreateState(id);
state.unread = true;
}
this.version++;
getCurrentWindow()
.setBadgeCount(ids.length)
.catch(() => {});
}
}

/**
* Remove unread entries for projects that no longer exist.
* Operates directly on persisted data so it works regardless of
* whether initFromStore() has run yet.
*/
async pruneDeletedProjects(existingProjectIds: Set<string>): Promise<void> {
const ids = await getStoreValue<string[]>(UNREAD_PROJECTS_STORE_KEY);
if (!ids || ids.length === 0) return;

const kept = ids.filter((id) => existingProjectIds.has(id));
if (kept.length === ids.length) return;

await setStoreValue(UNREAD_PROJECTS_STORE_KEY, kept);

// Also update in-memory state if it has been initialized
for (const id of ids) {
if (!existingProjectIds.has(id)) {
const state = this.states.get(id);
if (state) state.unread = false;
}
}
this.version++;
getCurrentWindow()
.setBadgeCount(kept.length || undefined)
.catch(() => {});
}
}

export const projectStateStore = new ProjectStateStore();