Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 204b8cc

Browse files
committed
Restructured
1 parent babd7cd commit 204b8cc

12 files changed

Lines changed: 837 additions & 273 deletions

File tree

webview-ui/src/App.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ import { CheckpointRestoreDialog } from "./components/chat/CheckpointRestoreDial
2020
import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
2121
import ErrorBoundary from "./components/ErrorBoundary"
2222
import { CloudView } from "./components/cloud/CloudView"
23-
import { WorktreesView } from "./components/worktrees"
2423
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
2524
import { TooltipProvider } from "./components/ui/tooltip"
2625
import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip"
2726

28-
type Tab = "settings" | "history" | "chat" | "marketplace" | "cloud" | "worktrees"
27+
type Tab = "settings" | "history" | "chat" | "marketplace" | "cloud"
2928

3029
interface DeleteMessageDialogState {
3130
isOpen: boolean
@@ -51,7 +50,6 @@ const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]
5150
historyButtonClicked: "history",
5251
marketplaceButtonClicked: "marketplace",
5352
cloudButtonClicked: "cloud",
54-
worktreesButtonClicked: "worktrees",
5553
}
5654

5755
const App = () => {
@@ -247,7 +245,6 @@ const App = () => {
247245
organizations={cloudOrganizations}
248246
/>
249247
)}
250-
{tab === "worktrees" && <WorktreesView onDone={() => switchTab("chat")} />}
251248
<ChatView
252249
ref={chatViewRef}
253250
isHidden={tab !== "chat"}

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import SystemPromptWarning from "./SystemPromptWarning"
4545
import ProfileViolationWarning from "./ProfileViolationWarning"
4646
import { CheckpointWarning } from "./CheckpointWarning"
4747
import { QueuedMessages } from "./QueuedMessages"
48+
import { WorktreeSelector } from "./WorktreeSelector"
4849
import DismissibleUpsell from "../common/DismissibleUpsell"
4950
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
5051
import { Cloud } from "lucide-react"
@@ -1521,6 +1522,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
15211522
{taskHistory.length < 6 && <RooTips />}
15221523
{/* Everyone should see their task history if any */}
15231524
{taskHistory.length > 0 && <HistoryPreview />}
1525+
{/* Worktree selector for quick switching between worktrees */}
1526+
<WorktreeSelector />
15241527
</div>
15251528
{/* Logged out users should see a one-time upsell, but not for brand new users */}
15261529
{!cloudIsAuthenticated && taskHistory.length >= 6 && (
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import React, { useState, useCallback, useEffect, useMemo } from "react"
2+
import { GitBranch, Check } from "lucide-react"
3+
4+
import type { Worktree, WorktreeListResponse } from "@roo-code/types"
5+
6+
import { cn } from "@/lib/utils"
7+
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
8+
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
9+
import { useAppTranslation } from "@/i18n/TranslationContext"
10+
import { vscode } from "@/utils/vscode"
11+
12+
import { IconButton } from "./IconButton"
13+
14+
interface WorktreeSelectorProps {
15+
disabled?: boolean
16+
}
17+
18+
export const WorktreeSelector = ({ disabled = false }: WorktreeSelectorProps) => {
19+
const { t } = useAppTranslation()
20+
const [open, setOpen] = useState(false)
21+
const [worktrees, setWorktrees] = useState<Worktree[]>([])
22+
const [isGitRepo, setIsGitRepo] = useState(true)
23+
const portalContainer = useRooPortal("roo-portal")
24+
25+
// Find current worktree
26+
const currentWorktree = useMemo(() => worktrees.find((w) => w.isCurrent), [worktrees])
27+
28+
// Fetch worktrees when popover opens
29+
const fetchWorktrees = useCallback(() => {
30+
vscode.postMessage({ type: "listWorktrees" })
31+
}, [])
32+
33+
// Handle messages from extension
34+
useEffect(() => {
35+
const handleMessage = (event: MessageEvent) => {
36+
const message = event.data
37+
if (message.type === "worktreeList") {
38+
const response: WorktreeListResponse = message
39+
setWorktrees(response.worktrees || [])
40+
setIsGitRepo(response.isGitRepo)
41+
}
42+
}
43+
44+
window.addEventListener("message", handleMessage)
45+
return () => window.removeEventListener("message", handleMessage)
46+
}, [])
47+
48+
// Initial fetch and refresh on open
49+
useEffect(() => {
50+
fetchWorktrees()
51+
}, [fetchWorktrees])
52+
53+
useEffect(() => {
54+
if (open) {
55+
fetchWorktrees()
56+
}
57+
}, [open, fetchWorktrees])
58+
59+
const handleSelect = useCallback((worktreePath: string) => {
60+
vscode.postMessage({
61+
type: "switchWorktree",
62+
worktreePath: worktreePath,
63+
worktreeNewWindow: false,
64+
})
65+
setOpen(false)
66+
}, [])
67+
68+
const handleSettingsClick = useCallback(() => {
69+
vscode.postMessage({
70+
type: "switchTab",
71+
tab: "settings",
72+
values: { section: "worktrees" },
73+
})
74+
setOpen(false)
75+
}, [])
76+
77+
// Don't render if not a git repo or only one worktree
78+
if (!isGitRepo || worktrees.length <= 1) {
79+
return null
80+
}
81+
82+
const title = t("worktrees:selector.tooltip")
83+
const instructionText = t("worktrees:selector.description")
84+
85+
return (
86+
<Popover open={open} onOpenChange={setOpen} data-testid="worktree-selector-root">
87+
<StandardTooltip content={title}>
88+
<PopoverTrigger
89+
disabled={disabled}
90+
data-testid="worktree-selector-trigger"
91+
className={cn(
92+
"inline-flex items-center relative whitespace-nowrap px-1.5 py-1 text-xs",
93+
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
94+
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
95+
disabled
96+
? "opacity-50 cursor-not-allowed"
97+
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
98+
)}>
99+
<GitBranch className="w-3 h-3 mr-1" />
100+
<span className="truncate">{currentWorktree?.branch || t("worktrees:noBranch")}</span>
101+
</PopoverTrigger>
102+
</StandardTooltip>
103+
<PopoverContent
104+
align="start"
105+
sideOffset={4}
106+
container={portalContainer}
107+
className="p-0 overflow-hidden min-w-80 max-w-9/10">
108+
<div className="flex flex-col w-full">
109+
{/* Info blurb */}
110+
<div className="p-3 border-b border-vscode-dropdown-border">
111+
<p className="m-0 text-xs text-vscode-descriptionForeground">{instructionText}</p>
112+
</div>
113+
114+
{/* Worktree list */}
115+
<div className="max-h-[300px] overflow-y-auto py-1">
116+
{worktrees.map((worktree) => {
117+
const isSelected = worktree.isCurrent
118+
return (
119+
<div
120+
key={worktree.path}
121+
onClick={() => !isSelected && handleSelect(worktree.path)}
122+
data-testid="worktree-selector-item"
123+
className={cn(
124+
"px-3 py-1.5 text-sm cursor-pointer flex items-center",
125+
"hover:bg-vscode-list-hoverBackground",
126+
isSelected &&
127+
"bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground",
128+
)}>
129+
<div className="flex-1 min-w-0">
130+
<div className="flex items-center gap-2">
131+
<GitBranch className="w-3 h-3 shrink-0" />
132+
<span className="font-bold truncate">
133+
{worktree.branch || t("worktrees:noBranch")}
134+
</span>
135+
{worktree.isBare && (
136+
<span className="text-xs opacity-70">{t("worktrees:primary")}</span>
137+
)}
138+
</div>
139+
<div className="text-xs text-vscode-descriptionForeground ml-5 truncate">
140+
{worktree.path}
141+
</div>
142+
</div>
143+
{isSelected && <Check className="ml-auto size-4 p-0.5" />}
144+
</div>
145+
)
146+
})}
147+
</div>
148+
149+
{/* Bottom bar with settings cog and title */}
150+
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
151+
<div className="flex flex-row gap-1">
152+
<IconButton
153+
iconClass="codicon-settings-gear"
154+
title={t("worktrees:selector.settings")}
155+
onClick={handleSettingsClick}
156+
/>
157+
</div>
158+
159+
{/* Info icon and title on the right */}
160+
<div className="flex items-center gap-1 pr-1">
161+
<StandardTooltip content={t("worktrees:selector.info")}>
162+
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
163+
</StandardTooltip>
164+
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
165+
{t("worktrees:selector.title")}
166+
</h4>
167+
</div>
168+
</div>
169+
</div>
170+
</PopoverContent>
171+
</Popover>
172+
)
173+
}

0 commit comments

Comments
 (0)