From 40c49b7484817e255704d3a2dbccca43ebcabdb0 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 22 Oct 2025 23:11:14 -0400 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20Add=20context=20menu=20for?= =?UTF-8?q?=20workspace=20rename=20and=20remove?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- bun.lock | 6 + package.json | 2 + src/components/WorkspaceListItem.tsx | 181 +++++++++++++++----------- src/components/ui/context-menu.tsx | 187 +++++++++++++++++++++++++++ 4 files changed, 300 insertions(+), 76 deletions(-) create mode 100644 src/components/ui/context-menu.tsx diff --git a/bun.lock b/bun.lock index ccdd07923..6267f43a6 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@ai-sdk/anthropic": "^2.0.29", "@ai-sdk/openai": "^2.0.52", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-scroll-area": "^1.2.10", @@ -26,6 +27,7 @@ "express": "^5.1.0", "jsonc-parser": "^3.3.1", "lru-cache": "^11.2.2", + "lucide-react": "^0.546.0", "markdown-it": "^14.1.0", "minimist": "^1.2.8", "source-map-support": "^0.5.21", @@ -423,6 +425,8 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "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=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "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=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "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 @@ "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], diff --git a/package.json b/package.json index bf78009bb..a87c762cc 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "dependencies": { "@ai-sdk/anthropic": "^2.0.29", "@ai-sdk/openai": "^2.0.52", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-scroll-area": "^1.2.10", @@ -66,6 +67,7 @@ "express": "^5.1.0", "jsonc-parser": "^3.3.1", "lru-cache": "^11.2.2", + "lucide-react": "^0.546.0", "markdown-it": "^14.1.0", "minimist": "^1.2.8", "source-map-support": "^0.5.21", diff --git a/src/components/WorkspaceListItem.tsx b/src/components/WorkspaceListItem.tsx index 6b99bf3c7..9b2c3eb9a 100644 --- a/src/components/WorkspaceListItem.tsx +++ b/src/components/WorkspaceListItem.tsx @@ -9,6 +9,12 @@ import { ModelDisplay } from "./Messages/ModelDisplay"; import { StatusIndicator } from "./StatusIndicator"; import { useRename } from "@/contexts/WorkspaceRenameContext"; import { cn } from "@/lib/utils"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; export interface WorkspaceSelection { projectPath: string; @@ -130,89 +136,112 @@ const WorkspaceListItemInner: React.FC = ({ return ( -
- onSelectWorkspace({ - projectPath, - projectName, - namedWorkspacePath, - workspaceId, - }) - } - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onSelectWorkspace({ - projectPath, - projectName, - namedWorkspacePath, - workspaceId, - }); - } - }} - role="button" - tabIndex={0} - aria-current={isSelected ? "true" : undefined} - data-workspace-path={namedWorkspacePath} - data-workspace-id={workspaceId} - > - - - - Remove workspace - - - - {isEditing ? ( - setEditingName(e.target.value)} - onKeyDown={handleRenameKeyDown} - onBlur={() => void handleConfirmRename()} - autoFocus - onClick={(e) => e.stopPropagation()} - aria-label={`Rename workspace ${displayName}`} - data-workspace-id={workspaceId} - /> - ) : ( - { + + + + Remove workspace + + + + {isEditing ? ( + setEditingName(e.target.value)} + onKeyDown={handleRenameKeyDown} + onBlur={() => void handleConfirmRename()} + autoFocus + onClick={(e) => e.stopPropagation()} + aria-label={`Rename workspace ${displayName}`} + data-workspace-id={workspaceId} + /> + ) : ( + { + e.stopPropagation(); + startRenaming(); + }} + title="Double-click to rename" + > + {displayName} + + )} + +
+ + + { e.stopPropagation(); startRenaming(); }} - title="Double-click to rename" > - {displayName} - - )} - - + Rename + + { + e.stopPropagation(); + void onRemoveWorkspace(workspaceId, e.currentTarget as HTMLElement); + }} + className="text-error focus:text-error" + > + Remove + + + {renameError && isEditing && (
{renameError} diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx new file mode 100644 index 000000000..44ddddfce --- /dev/null +++ b/src/components/ui/context-menu.tsx @@ -0,0 +1,187 @@ +import * as React from "react"; +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const ContextMenu = ContextMenuPrimitive.Root; + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; + +const ContextMenuGroup = ContextMenuPrimitive.Group; + +const ContextMenuPortal = ContextMenuPrimitive.Portal; + +const ContextMenuSub = ContextMenuPrimitive.Sub; + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; + +const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +ContextMenuShortcut.displayName = "ContextMenuShortcut"; + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}; From c3426aa916c8acbfacc9344072f29397b5168626 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 22 Oct 2025 23:17:41 -0400 Subject: [PATCH 2/3] Fix context menu z-index to appear above sidebar - Update context menu z-index from z-50 to z-[1001] - Ensures context menu appears above sidebar (z-[100]/z-[1000]) - Fixes issue where menu was appearing behind project list --- src/components/ui/context-menu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx index 44ddddfce..349522a40 100644 --- a/src/components/ui/context-menu.tsx +++ b/src/components/ui/context-menu.tsx @@ -44,7 +44,7 @@ const ContextMenuSubContent = React.forwardRef< Date: Wed, 22 Oct 2025 23:22:13 -0400 Subject: [PATCH 3/3] Add shadcn/UI standard CSS variables - Add standard shadcn CSS variables (--popover, --accent, etc.) - Map to existing cmux color tokens for consistency - Allows shadcn components to work out of the box - Context menu now has proper background and styling --- src/styles/globals.css | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/styles/globals.css b/src/styles/globals.css index 35e5d11d9..2a49a99ca 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -101,6 +101,27 @@ --color-separator: hsl(0 0% 15%); /* #252526 - separators */ --color-modal-bg: hsl(0 0% 18%); /* #2d2d30 - modal backgrounds */ + /* Shadcn/UI Standard Variables */ + --background: hsl(0 0% 12%); /* Same as --color-background */ + --foreground: hsl(0 0% 83%); /* Same as --color-foreground */ + --popover: hsl(0 0% 11.5%); /* Same as --color-bg-dark */ + --popover-foreground: hsl(0 0% 83%); /* Same as --color-foreground */ + --card: hsl(0 0% 11.5%); /* Same as --color-bg-dark */ + --card-foreground: hsl(0 0% 83%); /* Same as --color-foreground */ + --primary: hsl(207 100% 40%); /* Same as --color-accent */ + --primary-foreground: hsl(0 0% 100%); /* White text on primary */ + --secondary: hsl(0 0% 42%); /* Same as --color-secondary */ + --secondary-foreground: hsl(0 0% 83%); /* Same as --color-foreground */ + --muted: hsl(0 0% 16.5%); /* Same as --color-bg-hover */ + --muted-foreground: hsl(0 0% 53%); /* Same as --color-muted */ + --accent: hsl(0 0% 16.5%); /* Same as --color-bg-hover */ + --accent-foreground: hsl(0 0% 83%); /* Same as --color-foreground */ + --destructive: hsl(0 70% 50%); /* Same as --color-error */ + --destructive-foreground: hsl(0 0% 100%); /* White text on destructive */ + --border: hsl(240 2% 25%); /* Same as --color-border */ + --input: hsl(240 2% 25%); /* Same as --color-border */ + --ring: hsl(207 100% 40%); /* Same as --color-accent */ + /* Radius */ --radius: 0.5rem; }