Skip to content

Commit 40c49b7

Browse files
committed
🤖 Add context menu for workspace rename and remove
- Install shadcn context-menu component and lucide-react - Wrap WorkspaceListItem in ContextMenu - Add right-click menu with Rename and Remove options - Reuses existing rename and remove functionality - Remove option styled in error color for clarity
1 parent 4850101 commit 40c49b7

File tree

4 files changed

+300
-76
lines changed

4 files changed

+300
-76
lines changed

bun.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"dependencies": {
77
"@ai-sdk/anthropic": "^2.0.29",
88
"@ai-sdk/openai": "^2.0.52",
9+
"@radix-ui/react-context-menu": "^2.2.16",
910
"@radix-ui/react-dialog": "^1.1.15",
1011
"@radix-ui/react-dropdown-menu": "^2.1.16",
1112
"@radix-ui/react-scroll-area": "^1.2.10",
@@ -26,6 +27,7 @@
2627
"express": "^5.1.0",
2728
"jsonc-parser": "^3.3.1",
2829
"lru-cache": "^11.2.2",
30+
"lucide-react": "^0.546.0",
2931
"markdown-it": "^14.1.0",
3032
"minimist": "^1.2.8",
3133
"source-map-support": "^0.5.21",
@@ -423,6 +425,8 @@
423425

424426
"@radix-ui/react-context": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
425427

428+
"@radix-ui/react-context-menu": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
429+
426430
"@radix-ui/react-dialog": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
427431

428432
"@radix-ui/react-direction": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
@@ -2087,6 +2091,8 @@
20872091

20882092
"lru-cache": ["[email protected]", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
20892093

2094+
"lucide-react": ["[email protected]", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
2095+
20902096
"lz-string": ["[email protected]", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
20912097

20922098
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"dependencies": {
4747
"@ai-sdk/anthropic": "^2.0.29",
4848
"@ai-sdk/openai": "^2.0.52",
49+
"@radix-ui/react-context-menu": "^2.2.16",
4950
"@radix-ui/react-dialog": "^1.1.15",
5051
"@radix-ui/react-dropdown-menu": "^2.1.16",
5152
"@radix-ui/react-scroll-area": "^1.2.10",
@@ -66,6 +67,7 @@
6667
"express": "^5.1.0",
6768
"jsonc-parser": "^3.3.1",
6869
"lru-cache": "^11.2.2",
70+
"lucide-react": "^0.546.0",
6971
"markdown-it": "^14.1.0",
7072
"minimist": "^1.2.8",
7173
"source-map-support": "^0.5.21",

src/components/WorkspaceListItem.tsx

Lines changed: 105 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { ModelDisplay } from "./Messages/ModelDisplay";
99
import { StatusIndicator } from "./StatusIndicator";
1010
import { useRename } from "@/contexts/WorkspaceRenameContext";
1111
import { cn } from "@/lib/utils";
12+
import {
13+
ContextMenu,
14+
ContextMenuContent,
15+
ContextMenuItem,
16+
ContextMenuTrigger,
17+
} from "@/components/ui/context-menu";
1218

1319
export interface WorkspaceSelection {
1420
projectPath: string;
@@ -130,89 +136,112 @@ const WorkspaceListItemInner: React.FC<WorkspaceListItemProps> = ({
130136

131137
return (
132138
<React.Fragment>
133-
<div
134-
className={cn(
135-
"py-1.5 px-3 pl-7 cursor-pointer grid grid-cols-[auto_auto_1fr_auto] gap-2 items-center border-l-[3px] border-transparent transition-all duration-150 text-[13px] relative hover:bg-bg-hover [&:hover_button]:opacity-100",
136-
isSelected && "bg-bg-hover border-l-[#569cd6]"
137-
)}
138-
onClick={() =>
139-
onSelectWorkspace({
140-
projectPath,
141-
projectName,
142-
namedWorkspacePath,
143-
workspaceId,
144-
})
145-
}
146-
onKeyDown={(e) => {
147-
if (e.key === "Enter" || e.key === " ") {
148-
e.preventDefault();
149-
onSelectWorkspace({
150-
projectPath,
151-
projectName,
152-
namedWorkspacePath,
153-
workspaceId,
154-
});
155-
}
156-
}}
157-
role="button"
158-
tabIndex={0}
159-
aria-current={isSelected ? "true" : undefined}
160-
data-workspace-path={namedWorkspacePath}
161-
data-workspace-id={workspaceId}
162-
>
163-
<TooltipWrapper inline>
164-
<button
165-
className="opacity-0 bg-transparent text-muted border-none cursor-pointer text-base p-0 w-5 h-5 flex items-center justify-center transition-all duration-200 flex-shrink-0 col-start-1 hover:text-foreground hover:bg-white/10 hover:rounded-sm"
166-
onClick={(e) => {
167-
e.stopPropagation();
168-
void onRemoveWorkspace(workspaceId, e.currentTarget);
139+
<ContextMenu>
140+
<ContextMenuTrigger asChild>
141+
<div
142+
className={cn(
143+
"py-1.5 px-3 pl-7 cursor-pointer grid grid-cols-[auto_auto_1fr_auto] gap-2 items-center border-l-[3px] border-transparent transition-all duration-150 text-[13px] relative hover:bg-bg-hover [&:hover_button]:opacity-100",
144+
isSelected && "bg-bg-hover border-l-[#569cd6]"
145+
)}
146+
onClick={() =>
147+
onSelectWorkspace({
148+
projectPath,
149+
projectName,
150+
namedWorkspacePath,
151+
workspaceId,
152+
})
153+
}
154+
onKeyDown={(e) => {
155+
if (e.key === "Enter" || e.key === " ") {
156+
e.preventDefault();
157+
onSelectWorkspace({
158+
projectPath,
159+
projectName,
160+
namedWorkspacePath,
161+
workspaceId,
162+
});
163+
}
169164
}}
170-
aria-label={`Remove workspace ${displayName}`}
165+
role="button"
166+
tabIndex={0}
167+
aria-current={isSelected ? "true" : undefined}
168+
data-workspace-path={namedWorkspacePath}
171169
data-workspace-id={workspaceId}
172170
>
173-
×
174-
</button>
175-
<Tooltip className="tooltip" align="right">
176-
Remove workspace
177-
</Tooltip>
178-
</TooltipWrapper>
179-
<GitStatusIndicator
180-
gitStatus={gitStatus}
181-
workspaceId={workspaceId}
182-
tooltipPosition="right"
183-
/>
184-
{isEditing ? (
185-
<input
186-
className="bg-input-bg text-input-text border border-input-border rounded-sm px-1 py-0.5 text-[13px] font-inherit outline-none min-w-0 text-right focus:border-input-border-focus"
187-
value={editingName}
188-
onChange={(e) => setEditingName(e.target.value)}
189-
onKeyDown={handleRenameKeyDown}
190-
onBlur={() => void handleConfirmRename()}
191-
autoFocus
192-
onClick={(e) => e.stopPropagation()}
193-
aria-label={`Rename workspace ${displayName}`}
194-
data-workspace-id={workspaceId}
195-
/>
196-
) : (
197-
<span
198-
className="text-foreground text-[14px] whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer px-1 py-0.5 rounded-sm transition-colors duration-200 min-w-0 text-right hover:bg-white/5"
199-
onDoubleClick={(e) => {
171+
<TooltipWrapper inline>
172+
<button
173+
className="opacity-0 bg-transparent text-muted border-none cursor-pointer text-base p-0 w-5 h-5 flex items-center justify-center transition-all duration-200 flex-shrink-0 col-start-1 hover:text-foreground hover:bg-white/10 hover:rounded-sm"
174+
onClick={(e) => {
175+
e.stopPropagation();
176+
void onRemoveWorkspace(workspaceId, e.currentTarget);
177+
}}
178+
aria-label={`Remove workspace ${displayName}`}
179+
data-workspace-id={workspaceId}
180+
>
181+
×
182+
</button>
183+
<Tooltip className="tooltip" align="right">
184+
Remove workspace
185+
</Tooltip>
186+
</TooltipWrapper>
187+
<GitStatusIndicator
188+
gitStatus={gitStatus}
189+
workspaceId={workspaceId}
190+
tooltipPosition="right"
191+
/>
192+
{isEditing ? (
193+
<input
194+
className="bg-input-bg text-input-text border border-input-border rounded-sm px-1 py-0.5 text-[13px] font-inherit outline-none min-w-0 text-right focus:border-input-border-focus"
195+
value={editingName}
196+
onChange={(e) => setEditingName(e.target.value)}
197+
onKeyDown={handleRenameKeyDown}
198+
onBlur={() => void handleConfirmRename()}
199+
autoFocus
200+
onClick={(e) => e.stopPropagation()}
201+
aria-label={`Rename workspace ${displayName}`}
202+
data-workspace-id={workspaceId}
203+
/>
204+
) : (
205+
<span
206+
className="text-foreground text-[14px] whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer px-1 py-0.5 rounded-sm transition-colors duration-200 min-w-0 text-right hover:bg-white/5"
207+
onDoubleClick={(e) => {
208+
e.stopPropagation();
209+
startRenaming();
210+
}}
211+
title="Double-click to rename"
212+
>
213+
{displayName}
214+
</span>
215+
)}
216+
<StatusIndicator
217+
className="ml-2"
218+
streaming={isStreaming}
219+
unread={isUnread}
220+
onClick={handleToggleUnread}
221+
title={statusTooltipTitle}
222+
/>
223+
</div>
224+
</ContextMenuTrigger>
225+
<ContextMenuContent>
226+
<ContextMenuItem
227+
onClick={(e) => {
200228
e.stopPropagation();
201229
startRenaming();
202230
}}
203-
title="Double-click to rename"
204231
>
205-
{displayName}
206-
</span>
207-
)}
208-
<StatusIndicator
209-
className="ml-2"
210-
streaming={isStreaming}
211-
unread={isUnread}
212-
onClick={handleToggleUnread}
213-
title={statusTooltipTitle}
214-
/>
215-
</div>
232+
Rename
233+
</ContextMenuItem>
234+
<ContextMenuItem
235+
onClick={(e) => {
236+
e.stopPropagation();
237+
void onRemoveWorkspace(workspaceId, e.currentTarget as HTMLElement);
238+
}}
239+
className="text-error focus:text-error"
240+
>
241+
Remove
242+
</ContextMenuItem>
243+
</ContextMenuContent>
244+
</ContextMenu>
216245
{renameError && isEditing && (
217246
<div className="absolute top-full left-7 right-8 mt-1 px-2 py-1.5 bg-error-bg border border-error rounded-sm text-error text-xs z-10">
218247
{renameError}

0 commit comments

Comments
 (0)