From 121fbb76549f8ebca68abbbcd7a0c702a4e78f1f Mon Sep 17 00:00:00 2001 From: Alejandro Date: Thu, 29 Jan 2026 21:08:34 +0100 Subject: [PATCH] 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 --- src/components/WindowToolbar.tsx | 59 +++++++++++++++++--------------- src/core/logic.ts | 46 +++++++++++++++++++++++++ src/core/state.ts | 15 ++++++++ 3 files changed, 93 insertions(+), 27 deletions(-) diff --git a/src/components/WindowToolbar.tsx b/src/components/WindowToolbar.tsx index b2b279d..5f6928f 100644 --- a/src/components/WindowToolbar.tsx +++ b/src/components/WindowToolbar.tsx @@ -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 - {/* Move to tab submenu - only show if multiple workspaces */} - {hasMultipleWorkspaces && ( - <> - - - + + + + Move to tab + + + + + New + + {otherWorkspaces.length > 0 && } + {otherWorkspaces.map((ws) => ( + handleMoveToWorkspace(ws.id)} > - - Move to tab - - - {otherWorkspaces.map((ws) => ( - handleMoveToWorkspace(ws.id)} - > - {ws.number} - {ws.label ? ` ${ws.label}` : ""} - - ))} - - - - )} + {ws.number} + {ws.label ? ` ${ws.label}` : ""} + + ))} + + {/* REQ/COUNT-specific actions */} {isSpellableWindow && ( diff --git a/src/core/logic.ts b/src/core/logic.ts index 07527c1..83aff8f 100644 --- a/src/core/logic.ts +++ b/src/core/logic.ts @@ -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 | null, diff --git a/src/core/state.ts b/src/core/state.ts index 98f8618..6775b1f 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -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,