Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/calm-repos-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"helmor": patch
---

Show repository directories in more repo selection surfaces:
- Show the selected or hovered repository path in the workspace-start repository picker.
- Show repository paths on repo-grouped workspace headers.
2 changes: 2 additions & 0 deletions src-tauri/src/models/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use super::db;
pub struct RepositoryCreateOption {
pub id: String,
pub name: String,
pub root_path: Option<String>,
pub remote: Option<String>,
pub remote_url: Option<String>,
pub default_branch: Option<String>,
Expand Down Expand Up @@ -134,6 +135,7 @@ pub fn list_repositories() -> Result<Vec<RepositoryCreateOption>> {
Ok(RepositoryCreateOption {
id: row.get(0)?,
name,
root_path,
remote: row.get(4)?,
remote_url: row.get(5)?,
forge_provider: row.get(6)?,
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/workspace/workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct WorkspaceSidebarRow {
pub directory_name: String,
pub repo_id: String,
pub repo_name: String,
pub repo_root_path: Option<String>,
pub repo_icon_src: Option<String>,
pub repo_initials: String,
pub state: WorkspaceState,
Expand Down Expand Up @@ -1124,6 +1125,7 @@ pub fn record_to_sidebar_row(record: WorkspaceRecord) -> WorkspaceSidebarRow {
directory_name: record.directory_name,
repo_id: record.repo_id,
repo_name: record.repo_name,
repo_root_path: record.root_path.clone(),
repo_icon_src: helpers::repo_icon_src_for_root_path(record.root_path.as_deref()),
repo_initials,
state: record.state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function createPreparedWorkspaceRow(
directoryName: prepared.directoryName,
repoId: repository.id,
repoName: repository.name,
repoRootPath: repository.rootPath ?? null,
repoIconSrc: repository.repoIconSrc ?? null,
repoInitials: repository.repoInitials ?? null,
state: prepared.state,
Expand Down
36 changes: 36 additions & 0 deletions src/features/navigation/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ describe("WorkspacesSidebar", () => {
{
id: "repo:repo-1",
label: "helmor",
repoRootPath: "/Users/aidan/dev/projects/helmor/.git/worktrees/isonoe",
tone: "pinned",
rows: [
{
Expand All @@ -552,6 +553,41 @@ describe("WorkspacesSidebar", () => {
},
];

it("shows the repository path in a capped-width wrapping tooltip on repo headers", async () => {
const user = userEvent.setup();

render(
<TooltipProvider delayDuration={0}>
<WorkspacesSidebar
groups={repoGroups}
archivedRows={[]}
sidebarGrouping="repo"
/>
</TooltipProvider>,
);

const header = screen
.getAllByRole("button", { name: /helmor/i })
.find((el) => el.tagName === "DIV");
expect(header).toBeDefined();

await user.hover(within(header as HTMLElement).getByText("helmor"));

const tooltip = (
await screen.findAllByText(
"/Users/aidan/dev/projects/helmor/.git/worktrees/isonoe",
)
).find(
(element) => element.getAttribute("data-slot") === "tooltip-content",
);
expect(tooltip).toBeDefined();
expect(tooltip).toHaveAttribute("data-side", "top");
expect(tooltip).toHaveAttribute("data-align", "start");
expect(tooltip).toHaveClass("max-w-64");
expect(tooltip).toHaveClass("whitespace-normal");
expect(tooltip).toHaveClass("break-all");
});

it("renders a `+` button on a repo group header that fires onCreateWorkspaceForRepo with the repo id", async () => {
const user = userEvent.setup();
const onCreateWorkspaceForRepo = vi.fn();
Expand Down
27 changes: 23 additions & 4 deletions src/features/navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -697,9 +697,12 @@ export const WorkspacesSidebar = memo(function WorkspacesSidebar({
const repoSampleRow: WorkspaceRow | undefined = isRepoGroup
? item.group.rows[0]
: undefined;
const repoRootPath = isRepoGroup
? (item.group.repoRootPath?.trim() ?? null)
: null;

const headerLabel = (
<span className="flex items-center gap-2">
<span className="flex min-w-0 items-center gap-2">
{isArchived ? (
<Archive
className="size-[14px] shrink-0 text-[var(--workspace-sidebar-status-backlog)]"
Expand All @@ -715,9 +718,25 @@ export const WorkspacesSidebar = memo(function WorkspacesSidebar({
) : (
<GroupIcon tone={item.group.tone} />
)}
<span>{item.group.label}</span>
<span className="min-w-0 truncate">{item.group.label}</span>
</span>
);
const visibleHeaderLabel =
repoRootPath && isRepoGroup ? (
<Tooltip>
<TooltipTrigger asChild>{headerLabel}</TooltipTrigger>
<TooltipContent
side="top"
align="start"
sideOffset={8}
className="max-w-64 whitespace-normal break-all font-mono text-[11px] leading-snug"
>
{repoRootPath}
</TooltipContent>
</Tooltip>
) : (
headerLabel
);

const headerClassName = cn(
"group/trigger flex w-full select-none items-center justify-between rounded-lg px-2 text-[13px] font-semibold tracking-[-0.01em] text-foreground hover:bg-accent/60 py-1",
Expand Down Expand Up @@ -761,7 +780,7 @@ export const WorkspacesSidebar = memo(function WorkspacesSidebar({
}
}}
>
{headerLabel}
{visibleHeaderLabel}
{onCreateWorkspaceForRepo ? (
<Tooltip>
<TooltipTrigger asChild>
Expand Down Expand Up @@ -804,7 +823,7 @@ export const WorkspacesSidebar = memo(function WorkspacesSidebar({
disabled={!item.canCollapse}
onClick={() => toggleSection(item.groupId)}
>
{headerLabel}
{visibleHeaderLabel}

{item.group.rows.length > 0 ? (
<span className="relative flex h-5 min-w-5 items-center justify-center">
Expand Down
2 changes: 2 additions & 0 deletions src/features/navigation/sidebar-projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ describe("regroupByRepo", () => {
status: "in-progress",
repoId: "repo-A",
repoName: "alpha",
repoRootPath: "/Users/me/repos/alpha",
},
],
},
Expand Down Expand Up @@ -330,6 +331,7 @@ describe("regroupByRepo", () => {
const result = regroupByRepo(fixture);
const repoGroups = result.filter((g) => g.id.startsWith(REPO_GROUP_PREFIX));
expect(repoGroups.map((g) => g.label)).toEqual(["alpha", "beta"]);
expect(repoGroups[0]?.repoRootPath).toBe("/Users/me/repos/alpha");
// progress (pendingCreation) + done + review rows for repo-A
// collapse into the alpha bucket. Pinned and backlog rows do NOT
// land here — they kept their own groups.
Expand Down
12 changes: 10 additions & 2 deletions src/features/navigation/sidebar-projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function regroupByRepo(groups: WorkspaceGroup[]): WorkspaceGroup[] {
const bucketOrder = new Map<string, number>();
const repoBuckets = new Map<
string,
{ label: string; rows: WorkspaceRow[] }
{ label: string; repoRootPath: string | null; rows: WorkspaceRow[] }
>();

let seen = 0;
Expand All @@ -171,10 +171,17 @@ export function regroupByRepo(groups: WorkspaceGroup[]): WorkspaceGroup[] {
: UNKNOWN_REPO_GROUP_ID;
let bucket = repoBuckets.get(bucketId);
if (!bucket) {
bucket = { label: row.repoName ?? "Unknown", rows: [] };
bucket = {
label: row.repoName ?? "Unknown",
repoRootPath: row.repoRootPath?.trim() || null,
rows: [],
};
repoBuckets.set(bucketId, bucket);
firstSeen.set(bucketId, seen++);
}
if (!bucket.repoRootPath) {
bucket.repoRootPath = row.repoRootPath?.trim() || null;
}
bucket.rows.push(row);
// Lowest non-zero `repoSidebarOrder` across the bucket's rows is
// the canonical bucket order. They should all agree (a single
Expand Down Expand Up @@ -209,6 +216,7 @@ export function regroupByRepo(groups: WorkspaceGroup[]): WorkspaceGroup[] {
return {
id: bucketId,
label: bucket.label,
repoRootPath: bucket.repoRootPath,
// Repo groups don't carry status semantics; reuse "pinned" as a
// neutral tone that won't render a status icon (the header will
// branch on group.id and render an avatar instead).
Expand Down
127 changes: 83 additions & 44 deletions src/features/workspace-start/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { ChevronDown, GitBranch, Laptop, Plus, Split, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import {
ChevronDown,
FolderOpen,
GitBranch,
Laptop,
Plus,
Split,
X,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { BranchPickerPopover } from "@/components/branch-picker";
import { TrafficLightSpacer } from "@/components/chrome/traffic-light-spacer";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -43,6 +51,67 @@ function defaultBranchPrefix(repo: RepositoryCreateOption | null): string {
}
}

function RepositoryMenuContent({
align,
repositories,
selectedRepository,
onSelectRepository,
}: {
align: "center" | "start";
repositories: RepositoryCreateOption[];
selectedRepository: RepositoryCreateOption | null;
onSelectRepository: (repository: RepositoryCreateOption) => void;
}) {
const [focusedRepoId, setFocusedRepoId] = useState<string | null>(null);
const locationRepo = useMemo(
() =>
repositories.find((repository) => repository.id === focusedRepoId) ??
selectedRepository ??
repositories[0] ??
null,
[focusedRepoId, repositories, selectedRepository],
);
const location = locationRepo?.rootPath?.trim() ?? null;

return (
<DropdownMenuContent
align={align}
className="w-[20rem] max-w-[calc(100vw-2rem)] overflow-hidden p-1"
>
{repositories.map((repository) => (
<DropdownMenuItem
key={repository.id}
onClick={() => onSelectRepository(repository)}
onFocus={() => setFocusedRepoId(repository.id)}
onPointerEnter={() => setFocusedRepoId(repository.id)}
className="gap-2"
>
<WorkspaceAvatar
repoIconSrc={repository.repoIconSrc}
repoInitials={repository.repoInitials}
repoName={repository.name}
title={repository.name}
className="size-5 rounded-md"
fallbackClassName="text-[8px]"
/>
<span className="min-w-0 flex-1 truncate">{repository.name}</span>
</DropdownMenuItem>
))}
{location ? (
<div className="mt-1 flex min-w-0 items-start gap-2 border-t border-border/50 px-2 py-1.5 text-[11px] text-muted-foreground">
<FolderOpen className="mt-0.5 size-3.5 shrink-0" strokeWidth={1.8} />
<span
className="min-w-0 break-all font-mono leading-snug"
title={location}
>
{location}
</span>
</div>
) : null}
</DropdownMenuContent>
);
}

type WorkspaceStartPageProps = {
repositories: RepositoryCreateOption[];
selectedRepository: RepositoryCreateOption | null;
Expand Down Expand Up @@ -297,27 +366,12 @@ export function WorkspaceStartPage({
/>
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="center" className="min-w-56">
{repositories.map((repository) => (
<DropdownMenuItem
key={repository.id}
onClick={() => onSelectRepository(repository)}
className="gap-2"
>
<WorkspaceAvatar
repoIconSrc={repository.repoIconSrc}
repoInitials={repository.repoInitials}
repoName={repository.name}
title={repository.name}
className="size-5 rounded-md"
fallbackClassName="text-[8px]"
/>
<span className="min-w-0 flex-1 truncate">
{repository.name}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
<RepositoryMenuContent
align="center"
repositories={repositories}
selectedRepository={selectedRepository}
onSelectRepository={onSelectRepository}
/>
</DropdownMenu>
<span
className={cn(
Expand Down Expand Up @@ -371,27 +425,12 @@ export function WorkspaceStartPage({
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-56">
{repositories.map((repository) => (
<DropdownMenuItem
key={repository.id}
onClick={() => onSelectRepository(repository)}
className="gap-2"
>
<WorkspaceAvatar
repoIconSrc={repository.repoIconSrc}
repoInitials={repository.repoInitials}
repoName={repository.name}
title={repository.name}
className="size-5 rounded-md"
fallbackClassName="text-[8px]"
/>
<span className="min-w-0 flex-1 truncate">
{repository.name}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
<RepositoryMenuContent
align="start"
repositories={repositories}
selectedRepository={selectedRepository}
onSelectRepository={onSelectRepository}
/>
</DropdownMenu>
) : null}
<DropdownMenu>
Expand Down
3 changes: 3 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type WorkspaceRow = {
directoryName?: string;
repoId?: string;
repoName?: string;
repoRootPath?: string | null;
repoIconSrc?: string | null;
repoInitials?: string | null;
state?: WorkspaceState;
Expand Down Expand Up @@ -112,6 +113,7 @@ export type WorkspaceRow = {
export type WorkspaceGroup = {
id: string;
label: string;
repoRootPath?: string | null;
tone: GroupTone;
rows: WorkspaceRow[];
};
Expand Down Expand Up @@ -208,6 +210,7 @@ export type BranchPrefixType = "username" | "custom" | "none";
export type RepositoryCreateOption = {
id: string;
name: string;
rootPath?: string | null;
remote?: string | null;
remoteUrl?: string | null;
defaultBranch?: string | null;
Expand Down
Loading