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
13 changes: 12 additions & 1 deletion src-tauri/src/commands/vcs/git/stash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,16 @@ pub fn git_create_stash(
repo_path: String,
message: Option<String>,
include_untracked: bool,
files: Option<Vec<String>>,
) -> Result<(), String> {
_git_create_stash(repo_path, message, include_untracked).into_string_error()
_git_create_stash(repo_path, message, include_untracked, files).into_string_error()
}

fn _git_create_stash(
repo_path: String,
message: Option<String>,
include_untracked: bool,
files: Option<Vec<String>>,
) -> Result<()> {
let repo_dir = Path::new(&repo_path);
let mut args = vec!["stash", "push"];
Expand All @@ -68,6 +70,15 @@ fn _git_create_stash(
args.push(msg);
}

if let Some(ref file_list) = files {
if !file_list.is_empty() {
args.push("--");
for file in file_list {
args.push(file);
}
}
}

let output = Command::new("git")
.current_dir(repo_dir)
.args(&args)
Expand Down
6 changes: 6 additions & 0 deletions src/features/file-system/controllers/file-watcher-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ export async function initializeFileWatcherListener() {
const { path, event_type } = event.payload;
const parentDir = await dirname(path);

window.dispatchEvent(
new CustomEvent("file-external-change", {
detail: { path, event_type },
}),
);

// Handle deleted files - refresh parent directory
if (event_type === "deleted") {
scheduleDirectoryRefresh(parentDir);
Expand Down
23 changes: 0 additions & 23 deletions src/features/version-control/git/components/actions-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Archive,
Download,
GitPullRequest,
RefreshCw,
Expand Down Expand Up @@ -27,7 +26,6 @@ interface GitActionsMenuProps {
hasGitRepo: boolean;
repoPath?: string;
onRefresh?: () => void;
onOpenStashManager?: () => void;
onOpenRemoteManager?: () => void;
onOpenTagManager?: () => void;
}
Expand All @@ -39,7 +37,6 @@ const GitActionsMenu = ({
hasGitRepo,
repoPath,
onRefresh,
onOpenStashManager,
onOpenRemoteManager,
onOpenTagManager,
}: GitActionsMenuProps) => {
Expand Down Expand Up @@ -92,11 +89,6 @@ const GitActionsMenu = ({
await onRefresh?.();
};

const handleStashManager = () => {
onOpenStashManager?.();
onClose();
};

const handleRemoteManager = () => {
onOpenRemoteManager?.();
onClose();
Expand Down Expand Up @@ -179,21 +171,6 @@ const GitActionsMenu = ({
<div className="my-1 border-border border-t"></div>

{/* Advanced Operations */}
<button
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
handleStashManager();
}}
className={cn(
"flex w-full items-center gap-2 px-3 py-1.5",
"ui-font text-left text-text text-xs hover:bg-hover",
)}
>
<Archive size={12} />
Manage Stashes
</button>

<button
onMouseDown={(e) => {
e.preventDefault();
Expand Down
16 changes: 11 additions & 5 deletions src/features/version-control/git/components/commit-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type MouseEvent, memo, useCallback, useEffect, useMemo, useRef, useStat
import { createPortal } from "react-dom";
import { getCommitDiff } from "@/features/version-control/git/controllers/git";
import { useGitStore } from "@/features/version-control/git/controllers/store";
import { cn } from "@/utils/cn";

interface GitCommitHistoryProps {
onViewCommitDiff?: (commitHash: string, filePath?: string) => void;
Expand Down Expand Up @@ -262,7 +263,7 @@ const FileItem = memo(({ file, onFileClick }: FileItemProps) => {

const GitCommitHistory = ({ onViewCommitDiff, repoPath }: GitCommitHistoryProps) => {
const { commits, hasMoreCommits, isLoadingMoreCommits, actions } = useGitStore();
const [isCollapsed, setIsCollapsed] = useState(true);
const [isCollapsed, setIsCollapsed] = useState(false);
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

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

Changing the default collapsed state from true to false for the commit history panel is a UX change that should be documented in the PR description. This affects the initial view state for all users.

Suggested change
const [isCollapsed, setIsCollapsed] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(true);

Copilot uses AI. Check for mistakes.
const [commitFiles, setCommitFiles] = useState<Record<string, any[]>>({});
const [loadingCommits, setLoadingCommits] = useState<Set<string>>(new Set());
const [copiedHashes, setCopiedHashes] = useState<Set<string>>(new Set());
Expand Down Expand Up @@ -508,9 +509,14 @@ const GitCommitHistory = ({ onViewCommitDiff, repoPath }: GitCommitHistoryProps)
}

return (
<div className="border-border border-b">
<div
className={cn(
"border-border border-b transition-[flex-grow] duration-200",
!isCollapsed ? "flex min-h-0 flex-1 flex-col" : "flex-none",
)}
>
<div
className="flex cursor-pointer items-center gap-2 bg-secondary-bg px-3 py-1 text-text-lighter hover:bg-hover"
className="flex shrink-0 cursor-pointer items-center gap-2 bg-secondary-bg px-3 py-1 text-text-lighter hover:bg-hover"
onClick={() => setIsCollapsed(!isCollapsed)}
>
{isCollapsed ? <ChevronRight size={10} /> : <ChevronDown size={10} />}
Expand All @@ -519,8 +525,8 @@ const GitCommitHistory = ({ onViewCommitDiff, repoPath }: GitCommitHistoryProps)
</div>

{!isCollapsed && (
<div className="relative">
<div ref={scrollContainerRef} className="max-h-60 overflow-y-auto bg-primary-bg">
<div className="relative flex min-h-0 flex-1 flex-col">
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto bg-primary-bg">
{commits.map((commit) => (
<CommitItem
key={commit.hash}
Expand Down
16 changes: 15 additions & 1 deletion src/features/version-control/git/components/file-item.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Minus, Plus, Trash2 } from "lucide-react";
import { Archive, Minus, Plus, Trash2 } from "lucide-react";
import type { MouseEvent } from "react";
import { cn } from "@/utils/cn";
import type { GitFile } from "../types/git";
Expand All @@ -11,6 +11,7 @@ interface GitFileItemProps {
onStage?: () => void;
onUnstage?: () => void;
onDiscard?: () => void;
onStash?: () => void;
disabled?: boolean;
}

Expand All @@ -21,6 +22,7 @@ export const GitFileItem = ({
onStage,
onUnstage,
onDiscard,
onStash,
disabled,
}: GitFileItemProps) => {
const fileName = file.path.split("/").pop() || file.path;
Expand Down Expand Up @@ -64,6 +66,18 @@ export const GitFileItem = ({
>
<Plus size={10} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onStash?.();
}}
disabled={disabled}
className="p-0.5 text-text-lighter hover:text-text disabled:opacity-50"
title="Stash file"
aria-label="Stash file"
>
<Archive size={10} />
</button>
{file.status !== "untracked" && (
<button
onClick={(e) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { type RefObject, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useOnClickOutside } from "usehooks-ts";
import { cn } from "@/utils/cn";

interface StashMessageModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (message: string) => Promise<void>;
title?: string;
placeholder?: string;
}

export const StashMessageModal = ({
isOpen,
onClose,
onConfirm,
title = "Create Stash",
placeholder = "Stash message...",
}: StashMessageModalProps) => {
const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const modalRef = useRef<HTMLDivElement>(null);

useOnClickOutside(modalRef as RefObject<HTMLElement>, onClose);

useEffect(() => {
if (isOpen) {
setMessage("");
setTimeout(() => inputRef.current?.focus(), 50);
}
}, [isOpen]);

const handleConfirm = async () => {
setIsLoading(true);
try {
await onConfirm(message);
onClose();
} catch (error) {
console.error("Failed to create stash:", error);
} finally {
setIsLoading(false);
}
};

if (!isOpen) return null;

return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div
ref={modalRef}
className={cn(
"w-80 rounded-lg border border-border bg-secondary-bg p-4 shadow-xl",
"fade-in zoom-in-95 animate-in duration-200",
)}
>
<h3 className="mb-3 font-medium text-sm text-text">{title}</h3>
<input
ref={inputRef}
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder={placeholder}
className={cn(
"mb-4 w-full rounded border border-border bg-primary-bg px-2 py-1.5",
"text-sm text-text focus:border-blue-500 focus:outline-none",
)}
onKeyDown={(e) => {
if (e.key === "Enter") handleConfirm();
if (e.key === "Escape") onClose();
}}
/>
<div className="flex justify-end gap-2">
<button
onClick={onClose}
className="rounded px-3 py-1 text-text-lighter text-xs hover:bg-hover hover:text-text"
>
Cancel
</button>
<button
onClick={handleConfirm}
disabled={isLoading}
className={cn(
"rounded bg-blue-600 px-3 py-1 text-white text-xs",
"hover:bg-blue-700 disabled:opacity-50",
)}
>
{isLoading ? "Stashing..." : "Stash"}
</button>
</div>
</div>
</div>,
document.body,
);
};
Loading
Loading