fix(mobile): use exact directory matching for session grouping (#1687)

* fix(mobile): use exact directory matching for session grouping

The new mobile sessions sheet used startsWith prefix matching to
assign sessions to projects, which caused child-directory sessions
(e.g. /root/repos/opencode) to be grouped into parent projects
(e.g. /root/repos). Switch to exact directory matching (project root
or registered worktree paths only) to match the desktop sidebar
behavior.

Also exclude sub-agent sessions (those with parentID) from the
totalSessions badge count so the displayed number reflects only
top-level sessions.

* fix: align mobile session project matching

---------

Co-authored-by: lilyzhaun <lilyzhaun@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
lilyzhaun
2026-06-23 21:27:54 +03:00
committed by GitHub
co-authored by lilyzhaun Bohdan Triapitsyn
parent 12835c7646
commit 307808bec2
+27 -22
View File
@@ -154,6 +154,20 @@ const pathBelongsToRoot = (path: string, root: string): boolean => {
);
};
const findExactWorktreeMatch = (project: ProjectMeta, normalizedDirectory: string): WorktreeMetadata | null => (
project.worktrees.find((worktree) => normalizePath(worktree.path) === normalizedDirectory) ?? null
);
const projectMatchesExactDirectory = (project: ProjectMeta, normalizedDirectory: string): boolean => (
normalizedDirectory === project.path || Boolean(findExactWorktreeMatch(project, normalizedDirectory))
);
const findExactProjectMatch = (projects: ProjectMeta[], directory: string): ProjectMeta | null => {
const normalizedDirectory = normalizePath(directory);
if (!normalizedDirectory) return null;
return projects.find((project) => projectMatchesExactDirectory(project, normalizedDirectory)) ?? null;
};
const sessionMatchesQuery = (session: Session, projectLabel: string, query: string): boolean => {
if (!query) return true;
const haystack = `${session.title ?? ''} ${session.id} ${getSessionDirectory(session)} ${projectLabel}`.toLowerCase();
@@ -662,12 +676,10 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
for (const session of sessions) {
const directory = getSessionDirectory(session);
if (!directory) continue;
const node = nodes.find((entry) => {
if (pathBelongsToRoot(directory, entry.project.path)) return true;
return entry.project.worktrees.some((worktree) => pathBelongsToRoot(directory, worktree.path));
});
const normalizedDirectory = normalizePath(directory);
const node = nodes.find((entry) => projectMatchesExactDirectory(entry.project, normalizedDirectory));
if (!node) continue;
const matchedWorktree = node.project.worktrees.find((entry) => pathBelongsToRoot(directory, entry.path));
const matchedWorktree = findExactWorktreeMatch(node.project, normalizedDirectory);
const bucket = matchedWorktree
? ensureBucket(node, matchedWorktree.path, matchedWorktree)
: ensureBucket(node, node.project.path, null);
@@ -677,7 +689,9 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
for (const node of nodes) {
for (const bucket of node.buckets) {
bucket.sessions.sort((a, b) => getSessionTimestamp(b) - getSessionTimestamp(a));
node.totalSessions += bucket.sessions.length;
for (const session of bucket.sessions) {
if (!getParentId(session)) node.totalSessions += 1;
}
}
}
@@ -816,10 +830,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
// Switching session switches the working directory (handled by
// setCurrentSession) — also move the active project so the rest of the app
// and the active highlight follow the selected session, not just the draft.
const project = projectsMeta.find((entry) => {
if (pathBelongsToRoot(directory ?? '', entry.path)) return true;
return entry.worktrees.some((worktree) => pathBelongsToRoot(directory ?? '', worktree.path));
});
const project = findExactProjectMatch(projectsMeta, directory ?? '');
if (project) setActiveProjectIdOnly(project.id);
void setCurrentSession(session.id, directory);
onOpenChange(false);
@@ -878,12 +889,9 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
const buildSessionContextLabel = React.useCallback(
(session: Session): string => {
const directory = getSessionDirectory(session);
const project = projectsMeta.find((entry) => {
if (pathBelongsToRoot(directory, entry.path)) return true;
return entry.worktrees.some((worktree) => pathBelongsToRoot(directory, worktree.path));
});
const project = findExactProjectMatch(projectsMeta, directory);
if (!project) return getProjectLabel(directory) || directory;
const matchedWorktree = project.worktrees.find((entry) => pathBelongsToRoot(directory, entry.path));
const matchedWorktree = findExactWorktreeMatch(project, normalizePath(directory));
if (matchedWorktree?.branch) return `${project.label} · ${matchedWorktree.branch}`;
return project.label;
},
@@ -915,10 +923,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
return sessions
.filter((session) => {
const directory = getSessionDirectory(session);
const project = projectsMeta.find((entry) => {
if (pathBelongsToRoot(directory, entry.path)) return true;
return entry.worktrees.some((worktree) => pathBelongsToRoot(directory, worktree.path));
});
const project = findExactProjectMatch(projectsMeta, directory);
return sessionMatchesQuery(session, project?.label ?? '', normalizedQuery);
})
.sort((a, b) => getSessionTimestamp(b) - getSessionTimestamp(a));
@@ -931,9 +936,9 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
.map((project) => ({
...project,
sessionCount: sessions.filter((session) => {
const directory = getSessionDirectory(session);
if (pathBelongsToRoot(directory, project.path)) return true;
return project.worktrees.some((worktree) => pathBelongsToRoot(directory, worktree.path));
if (getParentId(session)) return false;
const directory = normalizePath(getSessionDirectory(session));
return projectMatchesExactDirectory(project, directory);
}).length,
}));
}, [normalizedQuery, projectsMeta, sessions]);