feat: prioritize recent projects by explicit opens
This commit is contained in:
parent
1932ddcbe2
commit
68378c603c
7 changed files with 353 additions and 4 deletions
|
|
@ -15,6 +15,7 @@ import {
|
||||||
findMatchingWorktree,
|
findMatchingWorktree,
|
||||||
type WorktreeMatch,
|
type WorktreeMatch,
|
||||||
} from '../utils/navigation';
|
} from '../utils/navigation';
|
||||||
|
import { recordRecentProjectOpenPaths } from '../utils/recentProjectOpenHistory';
|
||||||
|
|
||||||
const logger = createLogger('Feature:RecentProjects:open');
|
const logger = createLogger('Feature:RecentProjects:open');
|
||||||
|
|
||||||
|
|
@ -92,6 +93,7 @@ export function useOpenRecentProject(): {
|
||||||
async (project: DashboardRecentProject): Promise<void> => {
|
async (project: DashboardRecentProject): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await openTarget(project.openTarget, project.associatedPaths);
|
await openTarget(project.openTarget, project.associatedPaths);
|
||||||
|
recordRecentProjectOpenPaths([project.primaryPath, ...project.associatedPaths]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to open recent project', error);
|
logger.error('Failed to open recent project', error);
|
||||||
}
|
}
|
||||||
|
|
@ -116,6 +118,7 @@ export function useOpenRecentProject(): {
|
||||||
}
|
}
|
||||||
|
|
||||||
await openSyntheticPath(selectedPath, [selectedPath]);
|
await openSyntheticPath(selectedPath, [selectedPath]);
|
||||||
|
recordRecentProjectOpenPaths([selectedPath]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to select project folder', error);
|
logger.error('Failed to select project folder', error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ import { buildTaskCountsByProject, normalizePath } from '@renderer/utils/pathNor
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import { adaptRecentProjectsSection } from '../adapters/RecentProjectsSectionAdapter';
|
import { adaptRecentProjectsSection } from '../adapters/RecentProjectsSectionAdapter';
|
||||||
|
import {
|
||||||
|
sortRecentProjectsByDisplayPriority,
|
||||||
|
subscribeRecentProjectOpenHistory,
|
||||||
|
} from '../utils/recentProjectOpenHistory';
|
||||||
import {
|
import {
|
||||||
getRecentProjectsClientSnapshot,
|
getRecentProjectsClientSnapshot,
|
||||||
loadRecentProjectsWithClientCache,
|
loadRecentProjectsWithClientCache,
|
||||||
|
|
@ -74,6 +78,7 @@ export function useRecentProjectsSection(
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [visibleProjects, setVisibleProjects] = useState(maxProjects);
|
const [visibleProjects, setVisibleProjects] = useState(maxProjects);
|
||||||
const [aliveTeams, setAliveTeams] = useState<string[]>([]);
|
const [aliveTeams, setAliveTeams] = useState<string[]>([]);
|
||||||
|
const [openHistoryVersion, setOpenHistoryVersion] = useState(0);
|
||||||
const hasFetchedTasksRef = useRef(globalTasksInitialized);
|
const hasFetchedTasksRef = useRef(globalTasksInitialized);
|
||||||
const recentProjectsRef = useRef<DashboardRecentProject[]>(initialSnapshot?.projects ?? []);
|
const recentProjectsRef = useRef<DashboardRecentProject[]>(initialSnapshot?.projects ?? []);
|
||||||
|
|
||||||
|
|
@ -144,6 +149,11 @@ export function useRecentProjectsSection(
|
||||||
}
|
}
|
||||||
}, [maxProjects, searchQuery]);
|
}, [maxProjects, searchQuery]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => subscribeRecentProjectOpenHistory(() => setOpenHistoryVersion((current) => current + 1)),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const taskCountsByProject = useMemo(() => buildTaskCountsByProject(globalTasks), [globalTasks]);
|
const taskCountsByProject = useMemo(() => buildTaskCountsByProject(globalTasks), [globalTasks]);
|
||||||
|
|
||||||
const activeTeamsByProject = useMemo(() => {
|
const activeTeamsByProject = useMemo(() => {
|
||||||
|
|
@ -170,12 +180,18 @@ export function useRecentProjectsSection(
|
||||||
const decoratedCards = useMemo(
|
const decoratedCards = useMemo(
|
||||||
() =>
|
() =>
|
||||||
adaptRecentProjectsSection({
|
adaptRecentProjectsSection({
|
||||||
projects: recentProjects,
|
projects: sortRecentProjectsByDisplayPriority(recentProjects),
|
||||||
taskCountsByProject,
|
taskCountsByProject,
|
||||||
activeTeamsByProject,
|
activeTeamsByProject,
|
||||||
tasksLoading: globalTasksLoading,
|
tasksLoading: globalTasksLoading,
|
||||||
}),
|
}),
|
||||||
[activeTeamsByProject, globalTasksLoading, recentProjects, taskCountsByProject]
|
[
|
||||||
|
activeTeamsByProject,
|
||||||
|
globalTasksLoading,
|
||||||
|
openHistoryVersion,
|
||||||
|
recentProjects,
|
||||||
|
taskCountsByProject,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredCards = useMemo(
|
const filteredCards = useMemo(
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export { RecentProjectsSection } from './ui/RecentProjectsSection';
|
export { RecentProjectsSection } from './ui/RecentProjectsSection';
|
||||||
|
export { recordRecentProjectOpenPaths } from './utils/recentProjectOpenHistory';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||||
|
|
||||||
|
import type { DashboardRecentProject } from '@features/recent-projects/contracts';
|
||||||
|
|
||||||
|
const RECENT_PROJECT_OPEN_HISTORY_KEY = 'recent-projects:open-history';
|
||||||
|
const RECENT_PROJECT_OPEN_HISTORY_EVENT = 'recent-projects:open-history-changed';
|
||||||
|
const OPEN_PRIORITY_WINDOW_MS = 1000 * 60 * 60 * 48;
|
||||||
|
const MAX_HISTORY_ENTRIES = 120;
|
||||||
|
|
||||||
|
interface RecentProjectOpenHistoryEntry {
|
||||||
|
path: string;
|
||||||
|
openedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecentProjectOpenHistoryState {
|
||||||
|
version: 1;
|
||||||
|
entries: RecentProjectOpenHistoryEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function canUseLocalStorage(): boolean {
|
||||||
|
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHistoryPath(projectPath: string): string | null {
|
||||||
|
const trimmed = projectPath.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizePath(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readHistoryState(): RecentProjectOpenHistoryState {
|
||||||
|
if (!canUseLocalStorage()) {
|
||||||
|
return { version: 1, entries: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(RECENT_PROJECT_OPEN_HISTORY_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return { version: 1, entries: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(raw) as Partial<RecentProjectOpenHistoryState>;
|
||||||
|
const entries = Array.isArray(parsed.entries) ? parsed.entries : [];
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
entries: entries
|
||||||
|
.filter(
|
||||||
|
(entry): entry is RecentProjectOpenHistoryEntry =>
|
||||||
|
!!entry &&
|
||||||
|
typeof entry.path === 'string' &&
|
||||||
|
typeof entry.openedAt === 'number' &&
|
||||||
|
Number.isFinite(entry.openedAt)
|
||||||
|
)
|
||||||
|
.map((entry) => ({
|
||||||
|
path: entry.path,
|
||||||
|
openedAt: entry.openedAt,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { version: 1, entries: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneEntries(
|
||||||
|
entries: readonly RecentProjectOpenHistoryEntry[]
|
||||||
|
): RecentProjectOpenHistoryEntry[] {
|
||||||
|
const byPath = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const normalizedPath = normalizeHistoryPath(entry.path);
|
||||||
|
if (!normalizedPath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byPath.set(normalizedPath, Math.max(byPath.get(normalizedPath) ?? 0, entry.openedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(byPath.entries())
|
||||||
|
.map(([historyPath, openedAt]) => ({ path: historyPath, openedAt }))
|
||||||
|
.sort((left, right) => right.openedAt - left.openedAt)
|
||||||
|
.slice(0, MAX_HISTORY_ENTRIES);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeHistoryEntries(entries: readonly RecentProjectOpenHistoryEntry[]): void {
|
||||||
|
if (!canUseLocalStorage()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextState: RecentProjectOpenHistoryState = {
|
||||||
|
version: 1,
|
||||||
|
entries: pruneEntries(entries),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(RECENT_PROJECT_OPEN_HISTORY_KEY, JSON.stringify(nextState));
|
||||||
|
window.dispatchEvent(new CustomEvent(RECENT_PROJECT_OPEN_HISTORY_EVENT));
|
||||||
|
} catch {
|
||||||
|
// Best-effort persistence only.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHistoryLookup(): Map<string, number> {
|
||||||
|
return new Map(readHistoryState().entries.map((entry) => [entry.path, entry.openedAt]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectPaths(
|
||||||
|
project: Pick<DashboardRecentProject, 'primaryPath' | 'associatedPaths'>
|
||||||
|
): string[] {
|
||||||
|
return [project.primaryPath, ...project.associatedPaths]
|
||||||
|
.map((projectPath) => normalizeHistoryPath(projectPath))
|
||||||
|
.filter((projectPath): projectPath is string => Boolean(projectPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordRecentProjectOpenPaths(
|
||||||
|
projectPaths: readonly string[],
|
||||||
|
openedAt: number = Date.now()
|
||||||
|
): void {
|
||||||
|
const normalizedPaths = Array.from(
|
||||||
|
new Set(
|
||||||
|
projectPaths
|
||||||
|
.map((projectPath) => normalizeHistoryPath(projectPath))
|
||||||
|
.filter((projectPath): projectPath is string => Boolean(projectPath))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (normalizedPaths.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = readHistoryState().entries;
|
||||||
|
writeHistoryEntries([
|
||||||
|
...existing,
|
||||||
|
...normalizedPaths.map((projectPath) => ({
|
||||||
|
path: projectPath,
|
||||||
|
openedAt,
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecentProjectLastOpenedAt(
|
||||||
|
project: Pick<DashboardRecentProject, 'primaryPath' | 'associatedPaths'>
|
||||||
|
): number {
|
||||||
|
const historyLookup = createHistoryLookup();
|
||||||
|
return getProjectPaths(project).reduce(
|
||||||
|
(latest, projectPath) => Math.max(latest, historyLookup.get(projectPath) ?? 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortRecentProjectsByDisplayPriority(
|
||||||
|
projects: readonly DashboardRecentProject[],
|
||||||
|
now: number = Date.now()
|
||||||
|
): DashboardRecentProject[] {
|
||||||
|
const historyLookup = createHistoryLookup();
|
||||||
|
|
||||||
|
const getLastOpenedAt = (
|
||||||
|
project: Pick<DashboardRecentProject, 'primaryPath' | 'associatedPaths'>
|
||||||
|
): number =>
|
||||||
|
getProjectPaths(project).reduce(
|
||||||
|
(latest, projectPath) => Math.max(latest, historyLookup.get(projectPath) ?? 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPriorityOpen = (openedAt: number): boolean =>
|
||||||
|
openedAt > 0 && now - openedAt <= OPEN_PRIORITY_WINDOW_MS;
|
||||||
|
|
||||||
|
return [...projects].sort((left, right) => {
|
||||||
|
const leftOpenedAt = getLastOpenedAt(left);
|
||||||
|
const rightOpenedAt = getLastOpenedAt(right);
|
||||||
|
const leftPriority = isPriorityOpen(leftOpenedAt);
|
||||||
|
const rightPriority = isPriorityOpen(rightOpenedAt);
|
||||||
|
|
||||||
|
if (leftPriority !== rightPriority) {
|
||||||
|
return leftPriority ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftPriority && rightPriority && leftOpenedAt !== rightOpenedAt) {
|
||||||
|
return rightOpenedAt - leftOpenedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.mostRecentActivity !== right.mostRecentActivity) {
|
||||||
|
return right.mostRecentActivity - left.mostRecentActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftOpenedAt !== rightOpenedAt) {
|
||||||
|
return rightOpenedAt - leftOpenedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left.name.localeCompare(right.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeRecentProjectOpenHistory(listener: () => void): () => void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return () => undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (): void => listener();
|
||||||
|
window.addEventListener(RECENT_PROJECT_OPEN_HISTORY_EVENT, handleChange);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(RECENT_PROJECT_OPEN_HISTORY_EVENT, handleChange);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetRecentProjectOpenHistoryForTests(): void {
|
||||||
|
if (!canUseLocalStorage()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.removeItem(RECENT_PROJECT_OPEN_HISTORY_KEY);
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { recordRecentProjectOpenPaths } from '@features/recent-projects/renderer';
|
||||||
import { cn } from '@renderer/lib/utils';
|
import { cn } from '@renderer/lib/utils';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import {
|
import {
|
||||||
|
|
@ -351,8 +352,20 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
||||||
}, [activeProjectValue, effectiveSelectedRepositoryId, projects, repositoryGroups, viewMode]);
|
}, [activeProjectValue, effectiveSelectedRepositoryId, projects, repositoryGroups, viewMode]);
|
||||||
|
|
||||||
const handleProjectValueChange = (id: string): void => {
|
const handleProjectValueChange = (id: string): void => {
|
||||||
if (viewMode === 'grouped') selectRepository(id);
|
if (viewMode === 'grouped') {
|
||||||
else setActiveProject(id);
|
const repositoryGroup = repositoryGroups.find((repo) => repo.id === id);
|
||||||
|
if (repositoryGroup) {
|
||||||
|
recordRecentProjectOpenPaths(repositoryGroup.worktrees.map((worktree) => worktree.path));
|
||||||
|
}
|
||||||
|
selectRepository(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = projects.find((candidate) => candidate.id === id);
|
||||||
|
if (project?.path) {
|
||||||
|
recordRecentProjectOpenPaths([project.path]);
|
||||||
|
}
|
||||||
|
setActiveProject(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Worktree state
|
// Worktree state
|
||||||
|
|
@ -368,6 +381,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
||||||
const worktreeName = activeWorktree?.name ?? 'main';
|
const worktreeName = activeWorktree?.name ?? 'main';
|
||||||
|
|
||||||
const handleSelectWorktree = (worktree: Worktree): void => {
|
const handleSelectWorktree = (worktree: Worktree): void => {
|
||||||
|
recordRecentProjectOpenPaths([worktree.path]);
|
||||||
selectWorktree(worktree.id);
|
selectWorktree(worktree.id);
|
||||||
setIsWorktreeDropdownOpen(false);
|
setIsWorktreeDropdownOpen(false);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { recordRecentProjectOpenPaths } from '@features/recent-projects/renderer';
|
||||||
import { api, isElectronMode } from '@renderer/api';
|
import { api, isElectronMode } from '@renderer/api';
|
||||||
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
||||||
import { Badge } from '@renderer/components/ui/badge';
|
import { Badge } from '@renderer/components/ui/badge';
|
||||||
|
|
@ -475,10 +476,12 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
if (target.kind === 'grouped') {
|
if (target.kind === 'grouped') {
|
||||||
useStore.setState(getWorktreeNavigationState(target.repositoryId, target.worktreeId));
|
useStore.setState(getWorktreeNavigationState(target.repositoryId, target.worktreeId));
|
||||||
void useStore.getState().fetchSessionsInitial(target.worktreeId);
|
void useStore.getState().fetchSessionsInitial(target.worktreeId);
|
||||||
|
recordRecentProjectOpenPaths([projectPath]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
useStore.getState().selectProject(target.projectId);
|
useStore.getState().selectProject(target.projectId);
|
||||||
|
recordRecentProjectOpenPaths([projectPath]);
|
||||||
},
|
},
|
||||||
[projects, repositoryGroups]
|
[projects, repositoryGroups]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getRecentProjectLastOpenedAt,
|
||||||
|
recordRecentProjectOpenPaths,
|
||||||
|
resetRecentProjectOpenHistoryForTests,
|
||||||
|
sortRecentProjectsByDisplayPriority,
|
||||||
|
} from '@features/recent-projects/renderer/utils/recentProjectOpenHistory';
|
||||||
|
|
||||||
|
import type { DashboardRecentProject } from '@features/recent-projects/contracts';
|
||||||
|
|
||||||
|
function makeProject(
|
||||||
|
overrides: Partial<DashboardRecentProject> = {}
|
||||||
|
): DashboardRecentProject {
|
||||||
|
return {
|
||||||
|
id: 'repo:alpha',
|
||||||
|
name: 'alpha',
|
||||||
|
primaryPath: '/workspace/alpha',
|
||||||
|
associatedPaths: ['/workspace/alpha'],
|
||||||
|
mostRecentActivity: 1_000,
|
||||||
|
providerIds: ['anthropic'],
|
||||||
|
source: 'claude',
|
||||||
|
openTarget: {
|
||||||
|
type: 'existing-worktree',
|
||||||
|
repositoryId: 'repo-alpha',
|
||||||
|
worktreeId: 'wt-alpha',
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('recentProjectOpenHistory', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetRecentProjectOpenHistoryForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('records normalized paths and resolves the latest explicit open across associated paths', () => {
|
||||||
|
recordRecentProjectOpenPaths(['/Users/Test/Project/', '/users/test/project'], 5_000);
|
||||||
|
recordRecentProjectOpenPaths(['/Users/Test/Project/feature'], 8_000);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getRecentProjectLastOpenedAt(
|
||||||
|
makeProject({
|
||||||
|
primaryPath: '/users/test/project',
|
||||||
|
associatedPaths: ['/users/test/project', '/Users/Test/Project/feature'],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).toBe(8_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prioritizes explicitly opened projects ahead of raw activity during the priority window', () => {
|
||||||
|
const openedProject = makeProject({
|
||||||
|
id: 'repo:opened',
|
||||||
|
name: 'opened',
|
||||||
|
primaryPath: '/workspace/opened',
|
||||||
|
associatedPaths: ['/workspace/opened'],
|
||||||
|
mostRecentActivity: 5_000,
|
||||||
|
});
|
||||||
|
const activeProject = makeProject({
|
||||||
|
id: 'repo:active',
|
||||||
|
name: 'active',
|
||||||
|
primaryPath: '/workspace/active',
|
||||||
|
associatedPaths: ['/workspace/active'],
|
||||||
|
mostRecentActivity: 9_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
recordRecentProjectOpenPaths(['/workspace/opened'], 10_000);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
sortRecentProjectsByDisplayPriority([activeProject, openedProject], 11_000).map(
|
||||||
|
(project) => project.id
|
||||||
|
)
|
||||||
|
).toEqual(['repo:opened', 'repo:active']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to activity sorting after the explicit-open priority window expires', () => {
|
||||||
|
const openedProject = makeProject({
|
||||||
|
id: 'repo:opened',
|
||||||
|
name: 'opened',
|
||||||
|
primaryPath: '/workspace/opened',
|
||||||
|
associatedPaths: ['/workspace/opened'],
|
||||||
|
mostRecentActivity: 5_000,
|
||||||
|
});
|
||||||
|
const activeProject = makeProject({
|
||||||
|
id: 'repo:active',
|
||||||
|
name: 'active',
|
||||||
|
primaryPath: '/workspace/active',
|
||||||
|
associatedPaths: ['/workspace/active'],
|
||||||
|
mostRecentActivity: 9_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
recordRecentProjectOpenPaths(['/workspace/opened'], 10_000);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
sortRecentProjectsByDisplayPriority(
|
||||||
|
[activeProject, openedProject],
|
||||||
|
10_000 + 1000 * 60 * 60 * 72
|
||||||
|
).map((project) => project.id)
|
||||||
|
).toEqual(['repo:active', 'repo:opened']);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue