fix(ui): show project names in session hover cards - #4743
Conversation
Resolve each session's registered project name for sidebar hover details, falling back to the working directory basename while retaining the full path as secondary tooltip context. Fixes apache#4742 Generated-by: Codex
Astro-Han
left a comment
There was a problem hiding this comment.
Right fix. Reusing deriveTitlebarProjectName rather than adding a second name derivation is what makes it reviewable, keeping the absolute path on title means nothing is lost for mouse users, and moving the aria description to the same value keeps the spoken and visible text in agreement.
I checked the cases where the new value can be worse than the path it replaces. Most are not: a project that is archived or missing from the local snapshot falls back to the cwd basename rather than blanking, sessions outside the catalog fall back the same way, and the only input where the old code rendered something and the new one renders nothing is cwd === '/', which named nothing anyway. Three notes below, none blocking.
[P3] projectNameByIdentity is a fourth copy of one rule, and it resolves differently
@maka/core/project owns "resolve a project by id or alias":
export function findProjectByIdentity<T extends { readonly id: string; readonly aliases?: readonly string[] }>(
projects: readonly T[], identity: string,
): T | undefined {
return projects.find((project) => project.id === identity || project.aliases?.includes(identity));
}session-project-grouping.ts already builds two more index structures from the same input.projects in this same hook — canonicalProjectIds in deriveSessionNavigationGroups, and projectsById in deriveWorktreeSessionIds, which is already a Map<identity, ProjectRecord> and therefore already carries name. The new map is the fourth.
A map is a real improvement over an O(n) find per row, so this is not "just call the existing function". But the rule now diverges rather than merely repeating: names.set(alias, …) in a loop is last-write-wins, while find is first-match-wins. Two projects sharing an alias resolve to different names depending on which path asks. Relink is supposed to prevent shared aliases, so that specific input is a constructed one and I am not raising it as a defect — it is evidence that these are no longer the same rule.
Reusing the projectsById map that deriveWorktreeSessionIds already builds would give the O(1) lookup and one authority at the same time.
[P3] The change costs the ability to tell worktree sessions apart
A project's main checkout and each of its worktree sessions all carry the same projectId, and workspace-resolver.ts sets cwd from project.preferredPath. Their hover cards now show identical project-name lines, separated only by the boolean worktree badge, which does not name the worktree. The cwd used to separate them.
#4742 explicitly allows the path to become secondary tooltip detail, and the native tooltip does carry it — I confirmed Astryx's useHoverCard keeps the card open when the pointer moves into it and does not set pointer-events: none, so the path is reachable on hover. So this is a real reduction that the issue authorised, not a regression against it. worktreeSessionIds is already computed one line away if you want 项目名 · <basename> for those rows.
Related: for keyboard and screen-reader users the title is not reachable, and the aria description now carries the project name instead of the path, so the absolute path has no path at all in the sidebar for them. Given the issue's framing that is a trade rather than a loss, and the surrounding comments show the AT surface was considered — flagging it so it is a decision.
[P3] The new selector has a unit seam right next to it and the coverage went to E2E instead
session-navigation-controller.test.ts already asserts this exact selector shape:
assert.equal(controller().selectors.sessionMeta(linkedCatalog[2]!), 'Remote Mac');Two more lines there would cover sessionProjectName. As it stands the only new assertion is the E2E happy path, so alias resolution, the outside-catalog fallback, and projectId: null have no coverage in any tier. The checklist claim holds — the E2E does fail on main, because the class name does not exist — but the covered surface is one of four branches.
That also matters for placement: e2e/sidebar-project-row.spec.ts is on #4761's P2 list, so this grows a file scheduled to move, while the seam that would survive the move is already open.
Approving.


Summary
Fixes #4742
Verification
npm run lintnpm run format:checknpm run buildnpm --workspace @maka/ui run test:dist(364 passed)npm --workspace @maka/desktop run typechecknpx knip --workspace packages/uinpx knip --workspace apps/desktopnpm run check:renderer-architecture(71 passed)npx playwright test --config e2e/playwright.config.ts e2e/sidebar-project-row.spec.ts(4 passed)Visual verification used the deterministic Electron sidebar fixture: the task card now renders
示例项目; the absolute fixture path remains available only through the element title.AI use
Select exactly one:
Tool(s) and scope: Codex diagnosed the display path, implemented the project-name projection and fallback, and added regression coverage.
Checklist
Does this PR entail a change in behavior?