diff --git a/apps/web/components/task-create-dialog-repo-chips.test.tsx b/apps/web/components/task-create-dialog-repo-chips.test.tsx
index 9adb0b9c01..b03df5064b 100644
--- a/apps/web/components/task-create-dialog-repo-chips.test.tsx
+++ b/apps/web/components/task-create-dialog-repo-chips.test.tsx
@@ -273,7 +273,7 @@ describe("RepoChipsRow", () => {
);
fireEvent.click(screen.getByTestId("repo-chip-trigger"));
expect(screen.getByText("frontend")).toBeTruthy();
- expect(screen.getByText("projects/local-project")).toBeTruthy();
+ expect(screen.getByText("~/projects/local-project")).toBeTruthy();
expect(screen.queryByText("frontend-dup")).toBeNull();
});
@@ -299,7 +299,7 @@ describe("RepoChipsRow", () => {
/>,
);
fireEvent.click(screen.getByTestId("repo-chip-trigger"));
- fireEvent.click(screen.getByText("projects/local-project"));
+ fireEvent.click(screen.getByText("~/projects/local-project"));
expect(onRowRepositoryChange).toHaveBeenCalledWith("r0", "/home/me/projects/local-project");
});
diff --git a/apps/web/components/task-create-dialog-repo-chips.tsx b/apps/web/components/task-create-dialog-repo-chips.tsx
index e72c7d05e5..060b454511 100644
--- a/apps/web/components/task-create-dialog-repo-chips.tsx
+++ b/apps/web/components/task-create-dialog-repo-chips.tsx
@@ -3,12 +3,14 @@
import { useEffect, useMemo } from "react";
import { IconPlus, IconX, IconCode, IconGitBranch, IconGitFork } from "@tabler/icons-react";
import { cn, formatUserHomePath } from "@/lib/utils";
+import { Badge } from "@kandev/ui/badge";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useBranches, type BranchSource } from "@/hooks/domains/workspace/use-repository-branches";
import type { LocalRepository, Repository } from "@/lib/types/http";
import type { DialogFormState, TaskRepoRow } from "@/components/task-create-dialog-types";
import { autoSelectBranch } from "@/components/task-create-dialog-helpers";
import { scoreBranch } from "@/lib/utils/branch-filter";
+import { scoreRepo } from "@/lib/utils/repo-filter";
import {
Pill,
sortBranches,
@@ -461,11 +463,19 @@ function useRepoChipData({
const repoOptions: PillOption[] = useMemo(
() => [
- ...filteredRepos.map((r) => ({ value: r.id, label: r.name })),
+ ...filteredRepos.map((r) => ({
+ value: r.id,
+ label: r.name,
+ keywords: [r.name, r.local_path, formatUserHomePath(r.local_path)].filter(
+ (s): s is string => !!s,
+ ),
+ renderLabel: () => renderWorkspaceRepoOption(r),
+ })),
...filteredDiscovered.map((r) => ({
value: r.path,
- label: shortRepoPath(r.path),
- keywords: [r.path],
+ label: leafSegment(r.path),
+ keywords: [r.path, formatUserHomePath(r.path)],
+ renderLabel: () => renderDiscoveredRepoOption(r.path),
})),
],
[filteredRepos, filteredDiscovered],
@@ -546,6 +556,7 @@ function RepoChip({
emptyMessage="No repositories"
testId="repo-chip-trigger"
tooltip={repoTooltip}
+ filter={scoreRepo}
flat
/>
+ {repo.name}
+ {display ? (
+ {display}
+ ) : null}
+
+ );
+}
+
+function renderDiscoveredRepoOption(path: string) {
+ const display = formatUserHomePath(path);
+ return (
+
+
+ {leafSegment(path)}
+ {display}
+
+
+ on disk
+
+
+ );
+}
+
+function leafSegment(path: string): string {
+ const cleaned = path.replace(/\\/g, "/").replace(/\/+$/g, "");
+ const idx = cleaned.lastIndexOf("/");
+ return idx >= 0 ? cleaned.slice(idx + 1) : cleaned;
}
diff --git a/apps/web/lib/utils/repo-filter.test.ts b/apps/web/lib/utils/repo-filter.test.ts
new file mode 100644
index 0000000000..8714407f73
--- /dev/null
+++ b/apps/web/lib/utils/repo-filter.test.ts
@@ -0,0 +1,26 @@
+import { describe, it, expect } from "vitest";
+import { scoreRepo } from "./repo-filter";
+
+describe("scoreRepo", () => {
+ it("returns a positive score for an exact keyword match", () => {
+ expect(scoreRepo("uuid-123", "arthur", ["arthur"])).toBeGreaterThan(0);
+ });
+
+ it("returns a positive score for a substring/segment hit on the value", () => {
+ expect(scoreRepo("acme/widget-config", "widget")).toBeGreaterThan(0);
+ });
+
+ it("floors weak subsequence-only matches that scoreBranch would surface", () => {
+ // a-r-t-h-u-r appears in order across the path but no segment contains
+ // "arthur" — exactly the false positive scoreRepo is designed to kill.
+ expect(scoreRepo("playground/fun/thm/gameserver/lxd-alpine-builder", "arthur")).toBe(0);
+ });
+
+ it("returns a positive score when the search prefixes the value's leaf", () => {
+ expect(scoreRepo("playground/sky/arthur", "arth")).toBeGreaterThan(0);
+ });
+
+ it("returns 0 when nothing in value or keywords resembles the search", () => {
+ expect(scoreRepo("playground/foo", "completely-unrelated", ["foo"])).toBe(0);
+ });
+});
diff --git a/apps/web/lib/utils/repo-filter.ts b/apps/web/lib/utils/repo-filter.ts
new file mode 100644
index 0000000000..84c55e0191
--- /dev/null
+++ b/apps/web/lib/utils/repo-filter.ts
@@ -0,0 +1,16 @@
+import { scoreBranch } from "./branch-filter";
+
+// scoreRepo wraps scoreBranch but drops weak subsequence-only matches.
+//
+// scoreBranch's fuzzy fallback returns small non-zero scores when search
+// characters appear in order across an arbitrary haystack — useful for
+// branch typos, but noisy for repo paths. A query like "arthur" was
+// matching long paths like "playground/fun/thm/.../lxd-alpine-builder"
+// because a-r-t-h-u-r appears in order across path segments.
+//
+// scoreBranch awards >= 0.45 for any substring/prefix/word-boundary hit,
+// so a 0.4 floor preserves real matches while filtering coincidences.
+export function scoreRepo(value: string, search: string, keywords?: string[]): number {
+ const score = scoreBranch(value, search, keywords);
+ return score >= 0.4 ? score : 0;
+}