Plan: Replace shadcn ContextMenu with Tauri Native Menu in Runner Panel
Summary
Replace the web-based shadcn/ui ContextMenu components in RunnerPanel.tsx with Tauri v2’s native menu API (@tauri-apps/api/menu). This provides native OS context menus with system styling.
Important Discovery: Tauri v2 has built-in native menu support — no plugin needed! The tauri-plugin-context-menu is for Tauri v1 only.
Current Implementation Analysis
RunnerPanel.tsx Context Menus (2 locations)
1. File Tree Header Context Menu (lines 335-347)
| Item | Action | |——|——–| | New File… | Opens new file dialog | | New Folder… | Creates new folder | | Collapse All | Collapses all directories | | ─ (separator) | | | Show in File Explorer | Opens file in OS | | Refresh | Reloads file tree |
2. Tab Context Menu (lines 378-404)
| Item | Action | |——|——–| | Save | Saves current file | | ─ (separator) | | | Close | Closes current tab | | Close Others | Closes all other tabs | | Close to the Right | Closes tabs to the right | | Close All | Closes all tabs | | ─ (separator) | | | Show in File Explorer | Opens file in OS | | Rename… | Starts rename | | Copy Path | Copies path to clipboard | | ─ (separator) | | | Delete | Deletes file |
Implementation Plan
Phase 1: Add Tauri Menu Permissions
- Add to
src-tauri/capabilities/default.json:{ "permissions": [ "menu:allow-new", "menu:allow-popup", "menu:allow-set-text", "menu:allow-set-enabled" ] }
Phase 2: Create Context Menu Utility
Create src/lib/context-menu.ts:
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu';
import { getCurrentWindow } from '@tauri-apps/api/window';
export interface MenuAction {
id: string;
label: string;
enabled?: boolean;
items?: MenuAction[];
separator?: boolean;
shortcut?: string;
action: () => void | Promise<void>;
}
/** Show a native context menu at the given position.
* Works in Tauri; falls back to no-op in browser/web. */
export async function showContextMenu(
actions: MenuAction[],
x: number,
y: number
): Promise<void> {
// Check if running in Tauri
if (!isTauri()) {
return;
}
// Build menu items recursively
const menuItems = await buildMenuItems(actions);
const menu = await Menu.new({ items: menuItems });
// Popup at position relative to window
const window = getCurrentWindow();
await menu.popup({ x, y }, window);
}
/** Build MenuItem[] from MenuAction[] */
async function buildMenuItems(actions: MenuAction[]): Promise<MenuItem[]> {
const items: MenuItem[] = [];
for (const action of actions) {
if (action.separator) {
items.push(await PredefinedMenuItem.new({ item: 'Separator' }));
} else if (action.items?.length) {
// Submenu - recursive
const submenu = await Submenu.new({
text: action.label,
items: await buildMenuItems(action.items),
});
items.push(submenu);
} else {
const item = await MenuItem.new({
id: action.id,
text: action.label,
action: action.action,
enabled: action.enabled ?? true,
});
items.push(item);
}
}
return items;
}
/** Detect Tauri runtime */
function isTauri(): boolean {
return typeof window !== 'undefined' && '__TAURI__' in window;
}
Phase 3: Update RunnerPanel.tsx
Replace shadcn ContextMenu with native menu calls:
3.1 File Tree Header Context Menu
Replace:
<ContextMenu>
<ContextMenuTrigger asChild>
<span className="text-xs font-medium text-muted-foreground cursor-default">Files</span>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={...}>...</ContextMenuItem>
...
</ContextMenuContent>
</ContextMenu>
With:
// On right-click handler for native menu
const handleFileTreeContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
showContextMenu([
{ id: 'new-file', label: 'New File...', action: () => { setNewFileParentDir(generatedDir); setShowNewFile(true); } },
{ id: 'new-folder', label: 'New Folder...', action: () => startNewFolder(generatedDir) },
{ id: 'collapse-all', label: 'Collapse All', action: () => setExpandedDirs(new Set()) },
{ id: 'sep1', label: '', separator: true },
{ id: 'reveal', label: 'Show in File Explorer', action: () => revealInExplorer(generatedDir) },
{ id: 'refresh', label: 'Refresh', action: loadFiles },
], e.clientX, e.clientY);
};
// In the JSX:
<span
className="text-xs font-medium text-muted-foreground cursor-default"
onContextMenu={handleFileTreeContextMenu}
>
Files
</span>
3.2 Tab Context Menu
Similar transformation for tab context menu at line 378-404.
Phase 4: Preserve shadcn ContextMenu as Fallback
Optionally keep shadcn ContextMenu for web/preview builds where native menu isn’t available:
// Detect Tauri vs web
const isTauri = typeof window !== 'undefined' && '__TAURI__' in window;
{isTauri ? (
// Native menu via onContextMenu
<button onContextMenu={handleTabContextMenu} ... />
) : (
// shadcn ContextMenu for web preview
<ContextMenu>
...
</ContextMenu>
)}
Action Mapping Reference
| Original Action | Native Menu Equivalent |
|---|---|
<ContextMenu> + <ContextMenuTrigger asChild> |
onContextMenu handler |
<ContextMenuContent> |
Menu.new({ items: [...] }) |
<ContextMenuItem onClick={fn}> |
MenuItem.new({ id, text, action: fn }) |
<ContextMenuSeparator /> |
PredefinedMenuItem.new({ item: 'Separator' }) |
disabled={bool} |
enabled: !bool |
className="text-destructive" |
Custom styling not supported — text in menu item |
Files to Modify
| File | Changes |
|---|---|
src-tauri/capabilities/default.json |
Add menu permissions |
src/lib/context-menu.ts |
NEW — native menu utility |
src/panels/RunnerPanel.tsx |
Replace ContextMenu with native |
Testing Checklist
- File tree header right-click shows native menu
- All menu items functional
- Tab right-click shows native menu
- Menu dismisses on click outside
- Works in both Tauri and web preview modes
- No regression in existing functionality