mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 23:16:50 +02:00
feat: add "+ new tab" option to move window menu (#229)
* feat: add "+ new tab" option to move window menu Adds a "+ New tab" option at the top of the "Move to tab" submenu in the window toolbar. This allows users to create a new tab and move the current window to it in a single action, streamlining the workflow. Changes: - Add moveWindowToNewWorkspace function in logic.ts - Expose moveWindowToNewWorkspace through useGrimoire hook - Always show "Move to tab" menu (not just when multiple tabs exist) - Add "+ New tab" option with Plus icon at top of submenu https://claude.ai/code/session_01Hy2vYBooPyrF7ZJCodcCav * refactor: simplify new tab menu item copy - Change menu text from "+ New tab" to "New" (icon already shows +) - Simplify toast to just "Moved to new tab" https://claude.ai/code/session_01Hy2vYBooPyrF7ZJCodcCav --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
CopyCheck,
|
||||
ArrowRightFromLine,
|
||||
ExternalLink,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
@@ -44,24 +45,29 @@ export function WindowToolbar({
|
||||
}: WindowToolbarProps) {
|
||||
const setEditMode = useSetAtom(commandLauncherEditModeAtom);
|
||||
const [showSpellDialog, setShowSpellDialog] = useState(false);
|
||||
const { state, moveWindowToWorkspace, setActiveWorkspace } = useGrimoire();
|
||||
const { state, moveWindowToWorkspace, moveWindowToNewWorkspace } =
|
||||
useGrimoire();
|
||||
|
||||
// Get workspaces for move action
|
||||
const otherWorkspaces = Object.values(state.workspaces)
|
||||
.filter((ws) => ws.id !== state.activeWorkspaceId)
|
||||
.sort((a, b) => a.number - b.number);
|
||||
const hasMultipleWorkspaces = Object.keys(state.workspaces).length > 1;
|
||||
|
||||
const handleMoveToWorkspace = (targetWorkspaceId: string) => {
|
||||
if (!window) return;
|
||||
const targetWorkspace = state.workspaces[targetWorkspaceId];
|
||||
moveWindowToWorkspace(window.id, targetWorkspaceId);
|
||||
setActiveWorkspace(targetWorkspaceId);
|
||||
toast.success(
|
||||
`Moved to tab ${targetWorkspace.number}${targetWorkspace.label ? ` (${targetWorkspace.label})` : ""}`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleMoveToNewTab = () => {
|
||||
if (!window) return;
|
||||
moveWindowToNewWorkspace(window.id);
|
||||
toast.success("Moved to new tab");
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
if (!window) return;
|
||||
|
||||
@@ -195,31 +201,30 @@ export function WindowToolbar({
|
||||
Pop out window
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Move to tab submenu - only show if multiple workspaces */}
|
||||
{hasMultipleWorkspaces && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger
|
||||
disabled={otherWorkspaces.length === 0}
|
||||
{/* Move to tab submenu */}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<ArrowRightFromLine className="size-4 mr-2" />
|
||||
Move to tab
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem onClick={handleMoveToNewTab}>
|
||||
<Plus className="size-4 mr-2" />
|
||||
New
|
||||
</DropdownMenuItem>
|
||||
{otherWorkspaces.length > 0 && <DropdownMenuSeparator />}
|
||||
{otherWorkspaces.map((ws) => (
|
||||
<DropdownMenuItem
|
||||
key={ws.id}
|
||||
onClick={() => handleMoveToWorkspace(ws.id)}
|
||||
>
|
||||
<ArrowRightFromLine className="size-4 mr-2" />
|
||||
Move to tab
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{otherWorkspaces.map((ws) => (
|
||||
<DropdownMenuItem
|
||||
key={ws.id}
|
||||
onClick={() => handleMoveToWorkspace(ws.id)}
|
||||
>
|
||||
{ws.number}
|
||||
{ws.label ? ` ${ws.label}` : ""}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</>
|
||||
)}
|
||||
{ws.number}
|
||||
{ws.label ? ` ${ws.label}` : ""}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
||||
{/* REQ/COUNT-specific actions */}
|
||||
{isSpellableWindow && (
|
||||
|
||||
@@ -227,6 +227,52 @@ export const moveWindowToWorkspace = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new workspace and moves a window to it.
|
||||
* Returns an object with the new state and the new workspace ID.
|
||||
*/
|
||||
export const moveWindowToNewWorkspace = (
|
||||
state: GrimoireState,
|
||||
windowId: string,
|
||||
): { state: GrimoireState; newWorkspaceId: string } => {
|
||||
const currentId = state.activeWorkspaceId;
|
||||
const currentWs = state.workspaces[currentId];
|
||||
|
||||
// Find next available workspace number
|
||||
const nextNumber = findLowestAvailableWorkspaceNumber(state.workspaces);
|
||||
|
||||
// Create new workspace ID
|
||||
const newWorkspaceId = uuidv4();
|
||||
|
||||
// Remove window from current workspace
|
||||
const newCurrentLayout = removeFromLayout(currentWs.layout, windowId);
|
||||
const newCurrentWindowIds = currentWs.windowIds.filter(
|
||||
(id) => id !== windowId,
|
||||
);
|
||||
|
||||
// Create new state with new workspace and moved window
|
||||
const newState: GrimoireState = {
|
||||
...state,
|
||||
activeWorkspaceId: newWorkspaceId,
|
||||
workspaces: {
|
||||
...state.workspaces,
|
||||
[currentId]: {
|
||||
...currentWs,
|
||||
layout: newCurrentLayout,
|
||||
windowIds: newCurrentWindowIds,
|
||||
},
|
||||
[newWorkspaceId]: {
|
||||
id: newWorkspaceId,
|
||||
number: nextNumber,
|
||||
layout: windowId,
|
||||
windowIds: [windowId],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { state: newState, newWorkspaceId };
|
||||
};
|
||||
|
||||
export const updateLayout = (
|
||||
state: GrimoireState,
|
||||
layout: MosaicNode<string> | null,
|
||||
|
||||
@@ -236,6 +236,20 @@ export const useGrimoire = () => {
|
||||
[setState],
|
||||
);
|
||||
|
||||
const moveWindowToNewWorkspace = useCallback(
|
||||
(windowId: string): number => {
|
||||
let newWorkspaceNumber = 0;
|
||||
setState((prev) => {
|
||||
const result = Logic.moveWindowToNewWorkspace(prev, windowId);
|
||||
newWorkspaceNumber =
|
||||
result.state.workspaces[result.newWorkspaceId].number;
|
||||
return result.state;
|
||||
});
|
||||
return newWorkspaceNumber;
|
||||
},
|
||||
[setState],
|
||||
);
|
||||
|
||||
const updateLayout = useCallback(
|
||||
(layout: any) => {
|
||||
setState((prev) => Logic.updateLayout(prev, layout));
|
||||
@@ -385,6 +399,7 @@ export const useGrimoire = () => {
|
||||
updateWindow,
|
||||
removeWindow,
|
||||
moveWindowToWorkspace,
|
||||
moveWindowToNewWorkspace,
|
||||
updateLayout,
|
||||
setActiveWorkspace,
|
||||
setActiveAccount,
|
||||
|
||||
Reference in New Issue
Block a user