Skip to content
Merged
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
31 changes: 23 additions & 8 deletions src/main/lib/git/git-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { shell } from "electron";
import simpleGit from "simple-git";
import { z } from "zod";
import { publicProcedure, router } from "../trpc";
import { isUpstreamMissingError } from "./git-utils";
import {
isUpstreamMissingError,
isNonFastForwardPushError,
REMOTE_AHEAD_ERROR_PREFIX,
} from "./git-utils";
import { assertRegisteredWorktree } from "./security";
import { fetchGitHubPRStatus } from "./github";
import { gitCache } from "./cache";
Expand Down Expand Up @@ -247,13 +251,24 @@ export const createGitOperationsRouter = () => {
const git = createGitForNetwork(input.worktreePath);
const hasUpstream = await hasUpstreamBranch(git);

if (input.setUpstream && !hasUpstream) {
const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
await withLockRetry(input.worktreePath, () =>
git.push(["--set-upstream", "origin", branch.trim()])
);
} else {
await withLockRetry(input.worktreePath, () => git.push());
try {
if (input.setUpstream && !hasUpstream) {
const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
await withLockRetry(input.worktreePath, () =>
git.push(["--set-upstream", "origin", branch.trim()])
);
} else {
await withLockRetry(input.worktreePath, () => git.push());
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
if (isNonFastForwardPushError(message)) {
throw new Error(
`${REMOTE_AHEAD_ERROR_PREFIX} Remote has new commits. Pull with rebase and retry.`
);
}
throw error;
}
await git.fetch();
invalidateGitStateCaches(input.worktreePath);
Expand Down
11 changes: 11 additions & 0 deletions src/main/lib/git/git-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@ export function isUpstreamMissingError(message: string): boolean {
message.includes("couldn't find remote ref")
);
}

export function isNonFastForwardPushError(message: string): boolean {
return (
message.includes("[rejected]") ||
message.includes("non-fast-forward") ||
message.includes("fetch first") ||
message.includes("Updates were rejected")
);
}

export const REMOTE_AHEAD_ERROR_PREFIX = "REMOTE_AHEAD:";
3 changes: 2 additions & 1 deletion src/renderer/features/agents/main/active-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6187,7 +6187,7 @@ Make sure to preserve all functionality from both branches when resolving confli
onRefresh: handleCommitChangesRefresh,
})

const { push: pushBranch, isPending: isPushing } = usePushAction({
const { push: pushBranch, isPending: isPushing, dialog: pushDialog } = usePushAction({
worktreePath,
hasUpstream: gitStatus?.hasUpstream ?? true,
onSuccess: handleCommitChangesRefresh,
Expand Down Expand Up @@ -7450,6 +7450,7 @@ Make sure to preserve all functionality from both branches when resolving confli
return (
<FileOpenProvider onOpenFile={setFileViewerPath}>
<TextSelectionProvider>
{pushDialog}
{/* File Search Dialog (Cmd+P) */}
{worktreePath && (
<FileSearchDialog
Expand Down
198 changes: 198 additions & 0 deletions src/renderer/features/agents/ui/agent-tool-registry.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"use client"

import {
Activity,
Bell,
Clock,
Eye,
FileCode2,
FileText,
FolderSearch,
GitBranch,
List,
Expand All @@ -11,8 +15,10 @@ import {
Minimize2,
Plus,
RefreshCw,
Square,
Terminal,
XCircle,
Zap,
} from "lucide-react"
import {
CustomTerminalIcon,
Expand Down Expand Up @@ -607,6 +613,198 @@ export const AgentToolRegistry: Record<string, ToolMeta> = {
},
variant: "collapsible",
},

"tool-Skill": {
icon: SparklesIcon,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
const name = part.input?.skill || part.input?.name || "skill"
return isPending ? `Running ${name}` : `Ran ${name}`
},
subtitle: (part) => {
const args = part.input?.args
if (!args) return ""
const text = typeof args === "string" ? args : JSON.stringify(args)
return text.length > 50 ? text.slice(0, 47) + "..." : text
},
variant: "simple",
},

"tool-ScheduleWakeup": {
icon: Clock,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Scheduling wake-up" : "Scheduled wake-up"
},
subtitle: (part) => {
const seconds = part.input?.delaySeconds
if (typeof seconds !== "number") return ""
if (seconds < 60) return `${seconds}s`
if (seconds < 3600) return `${Math.round(seconds / 60)}m`
return `${Math.round(seconds / 360) / 10}h`
},
variant: "simple",
},

"tool-EnterPlanMode": {
icon: PlanningIcon,
title: (part) => {
const { isPending } = getToolStatus(part)
return isPending ? "Entering plan mode" : "Planning"
},
subtitle: () => "",
variant: "simple",
},

"tool-CronCreate": {
icon: Clock,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Creating schedule" : "Created schedule"
},
subtitle: (part) => {
const cron = part.input?.cron || part.input?.schedule || ""
return cron.length > 40 ? cron.slice(0, 37) + "..." : cron
},
variant: "simple",
},

"tool-CronDelete": {
icon: Clock,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Deleting schedule" : "Deleted schedule"
},
subtitle: (part) => {
const id = part.input?.id
return id ? `#${id}` : ""
},
variant: "simple",
},

"tool-CronList": {
icon: Clock,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Listing schedules" : "Listed schedules"
},
subtitle: () => "",
variant: "simple",
},

"tool-Monitor": {
icon: Activity,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Monitoring" : "Monitored"
},
subtitle: (part) => {
const cmd = part.input?.command || ""
return cmd.length > 40 ? cmd.slice(0, 37) + "..." : cmd
},
variant: "simple",
},

"tool-PushNotification": {
icon: Bell,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Sending notification" : "Sent notification"
},
subtitle: (part) => {
const title = part.input?.title || part.input?.message || ""
return title.length > 40 ? title.slice(0, 37) + "..." : title
},
variant: "simple",
},

"tool-TaskOutput": {
icon: FileText,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Getting task output" : "Got task output"
},
subtitle: (part) => {
const id = part.input?.taskId || part.input?.id
return id ? `#${id}` : ""
},
variant: "simple",
},

"tool-TaskStop": {
icon: Square,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Stopping task" : "Stopped task"
},
subtitle: (part) => {
const id = part.input?.taskId || part.input?.id
return id ? `#${id}` : ""
},
variant: "simple",
},

"tool-EnterWorktree": {
icon: GitBranch,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Entering worktree" : "Entered worktree"
},
subtitle: (part) => {
const branch = part.input?.branch || part.input?.path || ""
return branch.length > 40 ? branch.slice(0, 37) + "..." : branch
},
variant: "simple",
},

"tool-ExitWorktree": {
icon: GitBranch,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Leaving worktree" : "Left worktree"
},
subtitle: () => "",
variant: "simple",
},

"tool-RemoteTrigger": {
icon: Zap,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Triggering remote" : "Triggered remote"
},
subtitle: (part) => {
const name = part.input?.name || part.input?.trigger || ""
return name.length > 40 ? name.slice(0, 37) + "..." : name
},
variant: "simple",
},

"tool-ToolSearch": {
icon: SearchIcon,
title: (part) => {
const isPending =
part.state !== "output-available" && part.state !== "output-error"
return isPending ? "Finding tools" : "Found tools"
},
subtitle: (part) => {
const query = part.input?.query || ""
return query.length > 40 ? query.slice(0, 37) + "..." : query
},
variant: "simple",
},
}

// ============================================================================
Expand Down
11 changes: 7 additions & 4 deletions src/renderer/features/agents/ui/archive-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,17 @@ export const ArchivePopover = memo(function ArchivePopover({ trigger }: ArchiveP
},
)

// Remote archived chats (always fetch)
const { data: remoteArchivedChats, isLoading: isRemoteLoading } = useRemoteArchivedChats()
// Remote archived chats (lazy — only when popover is open)
const { data: remoteArchivedChats, isLoading: isRemoteLoading } = useRemoteArchivedChats(open)

// Loading if either is loading
const isLoading = isLocalLoading || isRemoteLoading

// Fetch all projects for git info (for local chats)
const { data: projects } = trpc.projects.list.useQuery(undefined)
// Fetch all projects for git info (for local chats) — lazy on open
const { data: projects } = trpc.projects.list.useQuery(undefined, {
enabled: open,
staleTime: 5 * 60 * 1000,
})

// Collect chat IDs for file stats query (only local chats)
const archivedChatIds = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function ChangesPanelHeader({
const [displayTime, setDisplayTime] = useState<string>("");
const timeoutRef = useRef<NodeJS.Timeout | null>(null);

const utils = trpc.useUtils();

const { data: branchData, refetch: refetchBranches } = trpc.changes.getBranches.useQuery(
{ worktreePath },
{ enabled: !!worktreePath },
Expand All @@ -59,6 +61,8 @@ export function ChangesPanelHeader({
const checkoutMutation = trpc.changes.checkout.useMutation({
onSuccess: () => {
refetchBranches();
utils.changes.getGitHubStatus.invalidate({ worktreePath });
utils.chats.getPrStatus.invalidate();
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export const DiffSidebarHeader = memo(function DiffSidebarHeader({
},
});

const { push: pushBranch, isPending: isPushPending } = usePushAction({
const { push: pushBranch, isPending: isPushPending, dialog: pushDialog } = usePushAction({
worktreePath,
hasUpstream,
onSuccess: onRefresh,
Expand Down Expand Up @@ -430,6 +430,8 @@ export const DiffSidebarHeader = memo(function DiffSidebarHeader({
: primaryAction;

return (
<>
{pushDialog}
<div className="relative flex items-center justify-between h-10 px-2 border-b border-border/50 bg-background flex-shrink-0">
{/* Drag region for window dragging */}
{isDesktop && !isFullscreen && (
Expand Down Expand Up @@ -961,5 +963,6 @@ export const DiffSidebarHeader = memo(function DiffSidebarHeader({
</DropdownMenu>
</div>
</div>
</>
);
})
Loading