Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert, querying only with the cwd is sufficient

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image actually, when revert to only query by 'cwd`, the pannel can't retrive the tasks of deleted worktree

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liangfung briefly, to reproduce this bug, you can : let's say your current workspace=xx/main, then create worktree(eg: br-1) with vscode, and DO NOT switch to this worktree; then, hover on the worktree label on Pochi's panel, click the revealed menu btn "create task" on the worktree, hence, the new task was created with : cwd = current workspace(xx/main), and git.worktree.gitdir = xx/br-1, later you can delete the worktree br-1.
so that when you fetch deleted tasks, query by cwd= xx/br-1 won't get the task.

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`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value of the worktree path should be the cwd. When filtering, compare the cwd with activeWorktrees.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

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),
Copy link
Copy Markdown
Contributor

@liangfung liangfung Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is recommended to query using activeWorktreePaths in the backend instead of filtering on the frontend

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

),
[worktrees, activeWorktrees],
);
return { deletedWorktrees };
}