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
30 changes: 27 additions & 3 deletions packages/livekit/src/livestore/task-queries.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { parseGitOriginUrl } from "@getpochi/common/git-utils";
import { Schema, queryDb, sql } from "@livestore/livestore";
import { tables } from "./default-schema";

export const makeTasksQuery = (cwd: string) =>
queryDb(
{
query: sql`select * from tasks where parentId is null and (cwd = '${cwd}' or git->>'$.worktree.gitdir' like '${cwd}/.git/worktrees%') order by updatedAt desc`,
query: sql`select * from tasks where parentId is null and (cwd = '${cwd}' or git->>'$.worktree.gitdir' = '${cwd}') order by updatedAt desc`,
schema: Schema.Array(tables.tasks.rowSchema),
},
{
Expand All @@ -21,7 +22,7 @@ export const makeTasksQuery = (cwd: string) =>
export const makeTasksWithLimitQuery = (cwd: string, limit: number) => {
return queryDb(
{
query: sql`select * from tasks where parentId is null and cwd = '${cwd}' order by updatedAt desc limit ${limit}`,
query: sql`select * from tasks where parentId is null and (cwd = '${cwd}' or git->>'$.worktree.gitdir' = '${cwd}') order by updatedAt desc limit ${limit}`,
schema: Schema.Array(tables.tasks.rowSchema),
},
{
Expand All @@ -38,7 +39,7 @@ export const makeTasksWithLimitQuery = (cwd: string, limit: number) => {
export const makeTasksCountQuery = (cwd: string) => {
return queryDb(
{
query: sql`select COUNT(*) as count from tasks where parentId is null and cwd = '${cwd}'`,
query: sql`select COUNT(*) as count from tasks where parentId is null and (cwd = '${cwd}' or git->>'$.worktree.gitdir' = '${cwd}')`,
schema: Schema.Array(Schema.Struct({ count: Schema.Number })),
},
{
Expand All @@ -47,3 +48,26 @@ export const makeTasksCountQuery = (cwd: string) => {
},
);
};

export const makeNonCwdWorktreesQuery = (cwd: string, origin?: string) => {
const originFilter = origin
? `and git->>'$.origin' = '${parseGitOriginUrl(origin)?.webUrl}'`
: "";

return queryDb(
{
query: sql`select distinct git->>'$.worktree.gitdir' as path, git->>'$.branch' as branch, cwd from tasks where parentId is null and cwd != '${cwd}' ${originFilter} and git->>'$.worktree.gitdir' is not null`,
schema: Schema.Array(
Schema.Struct({
path: Schema.String,
branch: Schema.String,
cwd: Schema.String,
}),
),
},
{
label: "tasks.cwd.worktrees",
deps: [cwd, origin],
},
);
};
65 changes: 61 additions & 4 deletions packages/vscode-webui/src/components/worktree-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from "@/components/ui/tooltip";
import { useSelectedModels } from "@/features/settings";
import { useCurrentWorkspace } from "@/lib/hooks/use-current-workspace";
import { useDeletedWorktrees } from "@/lib/hooks/use-deleted-worktrees";
import { usePochiTabs } from "@/lib/hooks/use-pochi-tabs";
import { useWorktrees } from "@/lib/hooks/use-worktrees";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -65,7 +66,7 @@ interface WorktreeGroup {
isMain: boolean;
createdAt?: number;
branch?: string;
data: GitWorktree["data"];
data?: GitWorktree["data"];
}

export function WorktreeList({
Expand All @@ -77,6 +78,7 @@ export function WorktreeList({
deletingWorktreePaths: Set<string>;
onDeleteWorktree: (worktreePath: string) => void;
}) {
const { t } = useTranslation();
const { data: currentWorkspace, isLoading: isLoadingCurrentWorkspace } =
useCurrentWorkspace();
const {
Expand All @@ -85,6 +87,7 @@ export function WorktreeList({
gitOriginUrl,
isLoading: isLoadingWorktrees,
} = useWorktrees();
const [showDeleted, setShowDeleted] = useState(false);

const groups = useMemo(() => {
if (isLoadingWorktrees || isLoadingCurrentWorkspace) {
Expand Down Expand Up @@ -147,18 +150,70 @@ export function WorktreeList({
return groups.filter((x) => !deletingWorktreePaths.has(x.path));
}, [groups, deletingWorktreePaths]);

const { deletedWorktrees } = useDeletedWorktrees({
cwd,
origin: gitOriginUrl,
activeWorktrees: worktrees,
});
const deletedGroups = useMemo(() => {
return R.pipe(
deletedWorktrees,
R.map((wt): WorktreeGroup => {
const name = getWorktreeNameFromWorktreePath(wt.path) || "unknown";

return {
path: wt.path,
createdAt: 0,
name,
isMain: false,
branch: wt.branch,
};
}),
);
}, [deletedWorktrees]);
return (
<div className="flex flex-col gap-1">
{optimisticGroups.map((group) => (
<WorktreeSection
isLoadingWorktrees={isLoadingWorktrees}
key={group.path}
group={group}
onDeleteGroup={onDeleteWorktree}
gitOriginUrl={gitOriginUrl}
gh={gh}
/>
))}
{deletedGroups.length > 0 && (
<>
<div className="group flex items-center py-2">
<div className="h-px flex-1 bg-border" />
<Button
variant="ghost"
size="sm"
className="mx-2 h-auto gap-2 py-0 text-muted-foreground text-xs hover:bg-transparent"
onClick={() => setShowDeleted(!showDeleted)}
>
<Trash2 className="size-3" />
<span className="w-0 overflow-hidden whitespace-nowrap transition-all group-hover:w-auto">
{showDeleted
? t("tasksPage.hideDeletedWorktrees")
: t("tasksPage.showDeletedWorktrees")}
</span>
</Button>
<div className="h-px flex-1 bg-border" />
</div>

{showDeleted &&
deletedGroups.map((group) => (
<WorktreeSection
key={group.path}
group={group}
gh={gh}
isDeleted={true}
gitOriginUrl={gitOriginUrl}
/>
))}
</>
)}
</div>
);
}
Expand All @@ -168,9 +223,10 @@ function WorktreeSection({
onDeleteGroup,
gh,
gitOriginUrl,
isDeleted,
}: {
group: WorktreeGroup;
isLoadingWorktrees: boolean;
isDeleted?: boolean;
onDeleteGroup?: (worktreePath: string) => void;
gh?: { installed: boolean; authorized: boolean };
gitOriginUrl?: string | null;
Expand Down Expand Up @@ -222,7 +278,7 @@ function WorktreeSection({
{prefixWorktreeName(group.name)}
</span>

<div className="mt-[1px] flex-1">
<div className={cn("mt-[1px] flex-1", isDeleted ? "hidden" : "")}>
{pullRequest ? (
<PrStatusDisplay
prNumber={pullRequest.id}
Expand All @@ -245,6 +301,7 @@ function WorktreeSection({
!isHovered && !showDeleteConfirm
? "pointer-events-none opacity-0"
: "opacity-100",
isDeleted ? "hidden" : "",
)}
>
<>
Expand Down
29 changes: 29 additions & 0 deletions packages/vscode-webui/src/lib/hooks/use-deleted-worktrees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { taskCatalog } from "@getpochi/livekit";
import { useStore } from "@livestore/react";
import { useMemo } from "react";

interface DeletedWorktreesOptions {
cwd: string;
origin?: string | null;
activeWorktrees?: { path: string }[];
}
export function useDeletedWorktrees({
cwd,
origin,
activeWorktrees,
}: DeletedWorktreesOptions) {
const { store } = useStore();

const worktreeQuery = useMemo(() => {
return taskCatalog.queries.makeNonCwdWorktreesQuery(cwd, origin ?? "");
}, [cwd, origin]);
const worktrees = store.useQuery(worktreeQuery);
const deletedWorktrees = useMemo(
() =>
worktrees.filter(
(wt) => !activeWorktrees?.some((active) => active.path === wt.path),
),
[worktrees, activeWorktrees],
);
return { deletedWorktrees };
}