Files
openchamber/packages/ui/src/lib/projectResolution.ts
T
f5b4a267c0 fix(sessions): route new sessions to the correct project when server omits directory (#1637, #2270) (#2272)
* fix(sessions): route new sessions to the correct project when server omits directory

createSession() passed the server response's `directory` field to
setCurrentSession as `directoryHint`. When the server response omitted
the field (a common path), `directoryHint` was `null` and
setCurrentSession fell back to `opencodeClient.getDirectory()`, which
could still hold a stale directory from a different project.

That fallback caused:
- #1637: clicking `+` on one project while browsing another created
  a session grouped under the wrong project.
- #2270: in a parent Git repo with multiple child projects (some
  without their own Git), sessions from a child with its own Git were
  grouped under a sibling child without Git.

Capture the effective directory passed to the SDK and reuse it as the
fallback for the server response. This guarantees `setCurrentSession`
and `registerSessionDirectory` always see the directory the user
explicitly selected for the new session.

Add regression tests covering both reported topologies and the
no-override / no-server-directory preservation path.

* fix(sessions): prefer registered project paths

---------

Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-07-17 13:14:22 +03:00

64 lines
2.3 KiB
TypeScript

import type { ProjectEntry } from "@/lib/api/types";
import type { WorktreeMetadata } from "@/types/worktree";
import { normalizePath } from "@/lib/pathNormalization";
export const normalizeProjectPath = normalizePath;
export const resolveProjectForDirectory = (
projects: ProjectEntry[],
directory: string | null,
): ProjectEntry | null => {
const nd = normalizeProjectPath(directory);
if (!nd) return null;
let best: ProjectEntry | null = null;
for (const p of projects) {
const pp = normalizeProjectPath(p.path);
if (!pp) continue;
if (nd !== pp && !nd.startsWith(`${pp}/`)) continue;
if (!best || pp.length > (normalizeProjectPath(best.path)?.length ?? 0)) best = p;
}
return best;
};
const resolveProjectFromWorktreeDirectory = (
projects: ProjectEntry[],
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
directory: string | null,
): ProjectEntry | null => {
const nd = normalizeProjectPath(directory);
if (!nd) return null;
let matchedWorktree: WorktreeMetadata | null = null;
let matchedProjectPath: string | null = null;
let bestLen = -1;
for (const [projectPath, worktrees] of availableWorktreesByProject.entries()) {
for (const wt of worktrees) {
const wp = normalizeProjectPath(wt.path);
if (!wp) continue;
if (nd !== wp && !nd.startsWith(`${wp}/`)) continue;
if (wp.length > bestLen) {
bestLen = wp.length;
matchedWorktree = wt;
matchedProjectPath = normalizeProjectPath(projectPath);
}
}
}
if (!matchedWorktree) return null;
const candidates = [normalizeProjectPath(matchedWorktree.projectDirectory), matchedProjectPath]
.filter((v): v is string => Boolean(v));
for (const c of candidates) {
const exact = projects.find((p) => normalizeProjectPath(p.path) === c) ?? null;
if (exact) return exact;
const nested = resolveProjectForDirectory(projects, c);
if (nested) return nested;
}
return null;
};
export const resolveProjectForSessionDirectory = (
projects: ProjectEntry[],
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
directory: string | null,
): ProjectEntry | null =>
resolveProjectForDirectory(projects, directory) ??
resolveProjectFromWorktreeDirectory(projects, availableWorktreesByProject, directory);