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
23 changes: 21 additions & 2 deletions crates/warpforge-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,18 +285,31 @@ pub enum Method {

// ── Diff / review ──
#[serde(rename = "diff.get")]
DiffGet { task_id: String },
DiffGet {
task_id: String,
/// Explicit repo path override (view any worktree). Falls back to the
/// task's worktree, then its project path.
#[serde(default)]
path: Option<String>,
},
#[serde(rename = "diff.resolveHunk")]
DiffResolveHunk {
task_id: String,
file: String,
hunk_index: u32,
resolution: HunkResolution,
#[serde(default)]
path: Option<String>,
},
/// Full old (HEAD) + new (working-tree) contents of one file — powers the
/// editable side-by-side (CodeMirror merge) review.
#[serde(rename = "file.contents")]
FileContents { task_id: String, path: String },
FileContents {
task_id: String,
path: String,
#[serde(default, rename = "repoPath")]
repo_path: Option<String>,
},
/// List files in the task's project working tree.
#[serde(rename = "file.list")]
FileList {
Expand All @@ -308,13 +321,17 @@ pub enum Method {
/// `@` picker does not — node_modules/target swamp it).
#[serde(default)]
include_ignored: bool,
#[serde(default)]
path: Option<String>,
},
/// Write new contents to a file in the task's working tree (in-review edit).
#[serde(rename = "file.save")]
FileSave {
task_id: String,
path: String,
content: String,
#[serde(default, rename = "repoPath")]
repo_path: Option<String>,
},
/// Stage files and commit them in the task's repo. `files=None` stages all
/// changes; `amend` rewrites the previous commit.
Expand All @@ -326,6 +343,8 @@ pub enum Method {
files: Option<Vec<String>>,
#[serde(default)]
amend: bool,
#[serde(default)]
path: Option<String>,
},
/// Pull the task's project repo up to its upstream (rebase + autostash).
/// Any conflict rolls the working tree back to the exact prior state.
Expand Down
15 changes: 14 additions & 1 deletion desktop/src/components/ChangesRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { daemon } from "../daemon";
import type { FileDiff } from "../protocol";
import { CommitBox } from "./changes/CommitBox";
import { FileTreeRow } from "./changes/FileTreeRow";
import { WorktreePicker } from "./changes/WorktreePicker";
import {
buildTree,
collectFolderKeys,
Expand Down Expand Up @@ -39,6 +40,8 @@ export function ChangesRail({
onCommitExpandedChange,
onCommitted,
onRefresh,
worktreePath,
onWorktreeChange,
}: {
project: string;
files: FileDiff[];
Expand All @@ -49,6 +52,8 @@ export function ChangesRail({
onCommitExpandedChange?: (expanded: boolean) => void;
onCommitted: () => void;
onRefresh: () => void;
worktreePath?: string | null;
onWorktreeChange?: (path: string | null) => void;
}) {
const allPaths = useMemo(() => files.map((f) => f.path), [files]);
const filesByPath = useMemo(() => new Map(files.map((f) => [f.path, f])), [files]);
Expand Down Expand Up @@ -228,7 +233,15 @@ export function ChangesRail({
return (
<div className="flex h-full min-h-0 flex-col bg-card">
<div className="flex h-11 items-center gap-2 border-b px-3 text-sm font-semibold">
<span className="min-w-0 flex-1 truncate">Changes</span>
<span className="shrink-0 truncate">Changes</span>
{onWorktreeChange && (
<WorktreePicker
project={project}
selectedPath={worktreePath ?? null}
onSelect={onWorktreeChange}
/>
)}
<span className="min-w-0 flex-1" />
<button
type="button"
aria-label="Refresh changes"
Expand Down
68 changes: 68 additions & 0 deletions desktop/src/components/changes/WorktreePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useQuery } from "@tanstack/react-query";
import { Check, ChevronDown, GitBranch } from "lucide-react";

import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";

import type { WorktreeInfo } from "../../protocol";
import { daemonQuery } from "../../query";

/**
* Compact worktree switcher for the Changes rail header. Lets the rail show the
* diff from the main project checkout or any active git worktree. Selecting a
* worktree passes its path up so the diff/file queries refetch from there.
*/
export function WorktreePicker({
project,
selectedPath,
onSelect,
}: {
project: string;
selectedPath: string | null;
onSelect: (path: string | null) => void;
}) {
const { data } = useQuery({
queryFn: daemonQuery<{ worktrees: WorktreeInfo[] }>("task.listWorktrees", { project }),
queryKey: ["worktrees", project],
});
const worktrees = data?.worktrees ?? [];

const selected = selectedPath ? worktrees.find((w) => w.path === selectedPath) : null;
const label = selected ? selected.branch : "Working tree";

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={`Worktree: ${label}`}
title="Switch worktree"
className="flex h-6 min-w-0 items-center gap-1 rounded px-1.5 text-xs text-muted-foreground hover:bg-secondary hover:text-foreground"
>
<GitBranch className="size-3.5 shrink-0 text-primary" />
<span className="max-w-28 truncate">{label}</span>
<ChevronDown className="size-3 shrink-0 opacity-60" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64">
<DropdownMenuItem onSelect={() => onSelect(null)}>
<span className="min-w-0 flex-1 truncate">Working tree</span>
<Check className={cn("size-3.5", selectedPath === null ? "opacity-100" : "opacity-0")} />
</DropdownMenuItem>
{worktrees.map((wt) => (
<DropdownMenuItem key={wt.path} onSelect={() => onSelect(wt.path)}>
<span className="min-w-0 flex-1 truncate">{wt.branch}</span>
<Check
className={cn("size-3.5", selectedPath === wt.path ? "opacity-100" : "opacity-0")}
/>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
5 changes: 4 additions & 1 deletion desktop/src/views/TaskDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export default function TaskDetail({ task, snapshot, onClose, onOpenTask, onOpen
const diffView = useUi((s) => s.diffView);
const setDiffView = useUi((s) => s.setDiffView);
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [worktreePath, setWorktreePath] = useState<string | null>(null);
const showChat = useUi((s) => s.showChat);
const showDiff = useUi((s) => s.showDiff);
const rightPanel = useUi((s) => s.rightPanel);
Expand Down Expand Up @@ -197,7 +198,7 @@ export default function TaskDetail({ task, snapshot, onClose, onOpenTask, onOpen
mentionFilesQuery,
fileDoc,
queryClient,
} = useTaskQueries(task.id, activeFile, activeTab, task.updatedAt);
} = useTaskQueries(task.id, activeFile, activeTab, task.updatedAt, worktreePath);

const setView = (v: "unified" | "split") => setDiffView(v);
const openFileTab = useCallback(
Expand Down Expand Up @@ -351,6 +352,8 @@ export default function TaskDetail({ task, snapshot, onClose, onOpenTask, onOpen
files={diff.files}
selected={selectedFile}
taskId={task.id}
worktreePath={worktreePath}
onWorktreeChange={setWorktreePath}
commitExpanded={commitExpanded}
onCommitExpandedChange={setCommitExpanded}
onCommitted={() => {
Expand Down
11 changes: 7 additions & 4 deletions desktop/src/views/task-detail/useTaskQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ export function useTaskQueries(
activeFile: string | null,
activeTab: ActiveTab,
updatedAt: number,
worktreePath?: string | null,
) {
const queryClient = useQueryClient();

const diffQuery = useQuery({
placeholderData: keepPreviousData,
queryFn: daemonQuery<TaskDiff>("diff.get", { task_id: taskId }),
queryKey: ["diff", taskId],
queryFn: daemonQuery<TaskDiff>("diff.get", { path: worktreePath, task_id: taskId }),
queryKey: ["diff", taskId, worktreePath ?? null],
refetchOnWindowFocus: "always",
});
const diff = diffQuery.data ?? null;
Expand All @@ -28,9 +29,10 @@ export function useTaskQueries(
placeholderData: keepPreviousData,
queryFn: daemonQuery<ProjectFile[]>("file.list", {
include_ignored: true,
path: worktreePath,
task_id: taskId,
}),
queryKey: ["fileList", taskId, "all"],
queryKey: ["fileList", taskId, "all", worktreePath ?? null],
});
const projectFiles = Array.isArray(fileListQuery.data) ? fileListQuery.data : EMPTY_PROJECT_FILES;
const fileListError = fileListQuery.error?.message ?? null;
Expand All @@ -51,8 +53,9 @@ export function useTaskQueries(
queryFn: daemonQuery<FileDoc>("file.contents", {
task_id: taskId,
path: activeFile,
repoPath: worktreePath,
}),
queryKey: ["fileContents", taskId, activeFile],
queryKey: ["fileContents", taskId, activeFile, worktreePath ?? null],
refetchOnWindowFocus: "always",
});
const fileDoc = fileContentsEnabled ? (fileDocQuery.data ?? null) : null;
Expand Down
Loading
Loading