diff --git a/cmd/wsh/cmd/wshcmd-web.go b/cmd/wsh/cmd/wshcmd-web.go index 12b6a82d4b..b01bc35e6f 100644 --- a/cmd/wsh/cmd/wshcmd-web.go +++ b/cmd/wsh/cmd/wshcmd-web.go @@ -39,9 +39,11 @@ var webGetInner bool var webGetAll bool var webGetJson bool var webOpenMagnified bool +var webOpenReplaceBlock string func init() { webOpenCmd.Flags().BoolVarP(&webOpenMagnified, "magnified", "m", false, "open view in magnified mode") + webOpenCmd.Flags().StringVarP(&webOpenReplaceBlock, "replace", "r", "", "replace block") webCmd.AddCommand(webOpenCmd) webGetCmd.Flags().BoolVarP(&webGetInner, "inner", "", false, "get inner html (instead of outer)") webGetCmd.Flags().BoolVarP(&webGetAll, "all", "", false, "get all matches (querySelectorAll)") @@ -98,6 +100,17 @@ func webOpenRun(cmd *cobra.Command, args []string) (rtnErr error) { sendActivity("web", rtnErr == nil) }() + var replaceBlockORef *waveobj.ORef + if webOpenReplaceBlock != "" { + var err error + replaceBlockORef, err = resolveSimpleId(webOpenReplaceBlock) + if err != nil { + return fmt.Errorf("resolving -r blockid: %w", err) + } + } + if replaceBlockORef != nil && webOpenMagnified { + return fmt.Errorf("cannot use --replace and --magnified together") + } wshCmd := wshrpc.CommandCreateBlockData{ BlockDef: &waveobj.BlockDef{ Meta: map[string]any{ @@ -107,6 +120,10 @@ func webOpenRun(cmd *cobra.Command, args []string) (rtnErr error) { }, Magnified: webOpenMagnified, } + if replaceBlockORef != nil { + wshCmd.TargetBlockId = replaceBlockORef.OID + wshCmd.TargetAction = wshrpc.CreateBlockAction_Replace + } oref, err := wshclient.CreateBlockCommand(RpcClient, wshCmd, nil) if err != nil { return fmt.Errorf("creating block: %w", err) diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index dcd161cf8a..8bfe03f01f 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -10,6 +10,7 @@ import { newLayoutNode, } from "@/layout/index"; import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks"; +import { LayoutTreeSplitHorizontalAction, LayoutTreeSplitVerticalAction } from "@/layout/lib/types"; import { getWebServerEndpoint } from "@/util/endpoints"; import { fetch } from "@/util/fetchutil"; import { deepCompareReturnPrev, getPrefixedSettings, isBlank } from "@/util/util"; @@ -379,6 +380,54 @@ function getApi(): ElectronApi { return (window as any).api; } +async function createBlockSplitHorizontally( + blockDef: BlockDef, + targetBlockId: string, + position: "before" | "after" +): Promise { + const tabId = globalStore.get(atoms.staticTabId); + const layoutModel = getLayoutModelForTabById(tabId); + const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } }; + const newBlockId = await ObjectService.CreateBlock(blockDef, rtOpts); + const targetNodeId = layoutModel.getNodeByBlockId(targetBlockId)?.id; + if (targetNodeId == null) { + throw new Error(`targetNodeId not found for blockId: ${targetBlockId}`); + } + const splitAction: LayoutTreeSplitHorizontalAction = { + type: LayoutTreeActionType.SplitHorizontal, + targetNodeId: targetNodeId, + newNode: newLayoutNode(undefined, undefined, undefined, { blockId: newBlockId }), + position: position, + focused: true, + }; + layoutModel.treeReducer(splitAction); + return newBlockId; +} + +async function createBlockSplitVertically( + blockDef: BlockDef, + targetBlockId: string, + position: "before" | "after" +): Promise { + const tabId = globalStore.get(atoms.staticTabId); + const layoutModel = getLayoutModelForTabById(tabId); + const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } }; + const newBlockId = await ObjectService.CreateBlock(blockDef, rtOpts); + const targetNodeId = layoutModel.getNodeByBlockId(targetBlockId)?.id; + if (targetNodeId == null) { + throw new Error(`targetNodeId not found for blockId: ${targetBlockId}`); + } + const splitAction: LayoutTreeSplitVerticalAction = { + type: LayoutTreeActionType.SplitVertical, + targetNodeId: targetNodeId, + newNode: newLayoutNode(undefined, undefined, undefined, { blockId: newBlockId }), + position: position, + focused: true, + }; + layoutModel.treeReducer(splitAction); + return newBlockId; +} + async function createBlock(blockDef: BlockDef, magnified = false, ephemeral = false): Promise { const tabId = globalStore.get(atoms.staticTabId); const layoutModel = getLayoutModelForTabById(tabId); @@ -682,6 +731,8 @@ export { countersClear, countersPrint, createBlock, + createBlockSplitHorizontally, + createBlockSplitVertically, createTab, fetchWaveFile, getAllBlockComponentModels, diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 142d4c5212..5a4cf9c583 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -4,6 +4,8 @@ import { atoms, createBlock, + createBlockSplitHorizontally, + createBlockSplitVertically, createTab, getAllBlockComponentModels, getApi, @@ -198,6 +200,58 @@ async function handleCmdN() { await createBlock(termBlockDef); } +async function handleSplitHorizontal() { + // split horizontally + const termBlockDef: BlockDef = { + meta: { + view: "term", + controller: "shell", + }, + }; + const layoutModel = getLayoutModelForStaticTab(); + const focusedNode = globalStore.get(layoutModel.focusedNode); + if (focusedNode == null) { + return; + } + const blockAtom = WOS.getWaveObjectAtom(WOS.makeORef("block", focusedNode.data?.blockId)); + const blockData = globalStore.get(blockAtom); + if (blockData?.meta?.view == "term") { + if (blockData?.meta?.["cmd:cwd"] != null) { + termBlockDef.meta["cmd:cwd"] = blockData.meta["cmd:cwd"]; + } + } + if (blockData?.meta?.connection != null) { + termBlockDef.meta.connection = blockData.meta.connection; + } + await createBlockSplitHorizontally(termBlockDef, focusedNode.data.blockId, "after"); +} + +async function handleSplitVertical() { + // split horizontally + const termBlockDef: BlockDef = { + meta: { + view: "term", + controller: "shell", + }, + }; + const layoutModel = getLayoutModelForStaticTab(); + const focusedNode = globalStore.get(layoutModel.focusedNode); + if (focusedNode == null) { + return; + } + const blockAtom = WOS.getWaveObjectAtom(WOS.makeORef("block", focusedNode.data?.blockId)); + const blockData = globalStore.get(blockAtom); + if (blockData?.meta?.view == "term") { + if (blockData?.meta?.["cmd:cwd"] != null) { + termBlockDef.meta["cmd:cwd"] = blockData.meta["cmd:cwd"]; + } + } + if (blockData?.meta?.connection != null) { + termBlockDef.meta.connection = blockData.meta.connection; + } + await createBlockSplitVertically(termBlockDef, focusedNode.data.blockId, "after"); +} + let lastHandledEvent: KeyboardEvent | null = null; function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean { @@ -280,6 +334,14 @@ function registerGlobalKeys() { handleCmdN(); return true; }); + globalKeyMap.set("Cmd:d", () => { + handleSplitHorizontal(); + return true; + }); + globalKeyMap.set("Shift:Cmd:d", () => { + handleSplitVertical(); + return true; + }); globalKeyMap.set("Cmd:i", () => { handleCmdI(); return true; diff --git a/frontend/layout/lib/layoutModel.ts b/frontend/layout/lib/layoutModel.ts index 636ba2402f..0e8887709d 100644 --- a/frontend/layout/lib/layoutModel.ts +++ b/frontend/layout/lib/layoutModel.ts @@ -17,7 +17,10 @@ import { insertNodeAtIndex, magnifyNodeToggle, moveNode, + replaceNode, resizeNode, + splitHorizontal, + splitVertical, swapNode, } from "./layoutTree"; import { @@ -35,8 +38,11 @@ import { LayoutTreeInsertNodeAtIndexAction, LayoutTreeMagnifyNodeToggleAction, LayoutTreeMoveNodeAction, + LayoutTreeReplaceNodeAction, LayoutTreeResizeNodeAction, LayoutTreeSetPendingAction, + LayoutTreeSplitHorizontalAction, + LayoutTreeSplitVerticalAction, LayoutTreeState, LayoutTreeSwapNodeAction, NavigateDirection, @@ -375,10 +381,18 @@ export class LayoutModel { case LayoutTreeActionType.MagnifyNodeToggle: magnifyNodeToggle(this.treeState, action as LayoutTreeMagnifyNodeToggleAction); break; - case LayoutTreeActionType.ClearTree: { + case LayoutTreeActionType.ClearTree: clearTree(this.treeState); break; - } + case LayoutTreeActionType.ReplaceNode: + replaceNode(this.treeState, action as LayoutTreeReplaceNodeAction); + break; + case LayoutTreeActionType.SplitHorizontal: + splitHorizontal(this.treeState, action as LayoutTreeSplitHorizontalAction); + break; + case LayoutTreeActionType.SplitVertical: + splitVertical(this.treeState, action as LayoutTreeSplitVerticalAction); + break; default: console.error("Invalid reducer action", this.treeState, action); } @@ -472,6 +486,81 @@ export class LayoutModel { ); break; } + case LayoutTreeActionType.ReplaceNode: { + const targetNode = this?.getNodeByBlockId(action.targetblockid); + if (!targetNode) { + console.error( + "Cannot apply eventbus layout action ReplaceNode, could not find target node with blockId", + action.targetblockid + ); + break; + } + const replaceAction: LayoutTreeReplaceNodeAction = { + type: LayoutTreeActionType.ReplaceNode, + targetNodeId: targetNode.id, + newNode: newLayoutNode(undefined, action.nodesize, undefined, { + blockId: action.blockid, + }), + }; + this.treeReducer(replaceAction, false); + break; + } + case LayoutTreeActionType.SplitHorizontal: { + const targetNode = this?.getNodeByBlockId(action.targetblockid); + if (!targetNode) { + console.error( + "Cannot apply eventbus layout action SplitHorizontal, could not find target node with blockId", + action.targetblockid + ); + break; + } + if (action.position != "before" && action.position != "after") { + console.error( + "Cannot apply eventbus layout action SplitHorizontal, invalid position", + action.position + ); + break; + } + const newNode = newLayoutNode(undefined, action.nodesize, undefined, { + blockId: action.blockid, + }); + const splitAction: LayoutTreeSplitHorizontalAction = { + type: LayoutTreeActionType.SplitHorizontal, + targetNodeId: targetNode.id, + newNode: newNode, + position: action.position, + }; + this.treeReducer(splitAction, false); + break; + } + case LayoutTreeActionType.SplitVertical: { + const targetNode = this?.getNodeByBlockId(action.targetblockid); + if (!targetNode) { + console.error( + "Cannot apply eventbus layout action SplitVertical, could not find target node with blockId", + action.targetblockid + ); + break; + } + if (action.position != "before" && action.position != "after") { + console.error( + "Cannot apply eventbus layout action SplitVertical, invalid position", + action.position + ); + break; + } + const newNode = newLayoutNode(undefined, action.nodesize, undefined, { + blockId: action.blockid, + }); + const splitAction: LayoutTreeSplitVerticalAction = { + type: LayoutTreeActionType.SplitVertical, + targetNodeId: targetNode.id, + newNode: newNode, + position: action.position, + }; + this.treeReducer(splitAction, false); + break; + } default: console.warn("unsupported layout action", action); break; diff --git a/frontend/layout/lib/layoutTree.ts b/frontend/layout/lib/layoutTree.ts index 256aa64103..e60771acf3 100644 --- a/frontend/layout/lib/layoutTree.ts +++ b/frontend/layout/lib/layoutTree.ts @@ -29,6 +29,9 @@ import { MoveOperation, } from "./types"; +import { newLayoutNode } from "./layoutNode"; +import { LayoutTreeReplaceNodeAction, LayoutTreeSplitHorizontalAction, LayoutTreeSplitVerticalAction } from "./types"; + export const DEFAULT_MAX_CHILDREN = 5; /** @@ -425,3 +428,118 @@ export function clearTree(layoutState: LayoutTreeState) { layoutState.magnifiedNodeId = undefined; layoutState.generation++; } + +export function replaceNode(layoutState: LayoutTreeState, action: LayoutTreeReplaceNodeAction) { + const { targetNodeId, newNode } = action; + if (layoutState.rootNode.id === targetNodeId) { + newNode.size = layoutState.rootNode.size; // preserve size + layoutState.rootNode = newNode; + } else { + const parent = findParent(layoutState.rootNode, targetNodeId); + if (!parent) { + console.error("replaceNode: Parent not found for", targetNodeId); + return; + } + const index = parent.children.findIndex((child) => child.id === targetNodeId); + if (index === -1) { + console.error("replaceNode: Target node not found in parent's children", targetNodeId); + return; + } + // Preserve the old node's size. + const targetNode = parent.children[index]; + newNode.size = targetNode.size; + parent.children[index] = newNode; + } + layoutState.generation++; +} + +// ─── SPLIT HORIZONTAL ───────────────────────────────────────────────────────────── + +export function splitHorizontal(layoutState: LayoutTreeState, action: LayoutTreeSplitHorizontalAction) { + const { targetNodeId, newNode, position } = action; + const targetNode = findNode(layoutState.rootNode, targetNodeId); + if (!targetNode) { + console.error("splitHorizontal: Target node not found", targetNodeId); + return; + } + + const parent = findParent(layoutState.rootNode, targetNodeId); + if (parent && parent.flexDirection === FlexDirection.Row) { + const index = parent.children.findIndex((child) => child.id === targetNodeId); + if (index === -1) { + console.error("splitHorizontal: Target node not found in parent's children", targetNodeId); + return; + } + const insertIndex = position === "before" ? index : index + 1; + // Directly splice in the new node instead of calling addChildAt (which may flatten nodes) + parent.children.splice(insertIndex, 0, newNode); + // Rebalance sizes equally (or use your own logic) + parent.children.forEach((child) => (child.size = 1)); + } else { + // Otherwise, if no parent or parent's flexDirection is not Row, we need to wrap + // Create a new group node with horizontal layout. + // IMPORTANT: pass an initial children array so the new node is valid. + const groupNode = newLayoutNode(FlexDirection.Row, targetNode.size, [targetNode], undefined); + // Now decide the ordering based on the "position" + groupNode.children = position === "before" ? [newNode, targetNode] : [targetNode, newNode]; + groupNode.children.forEach((child) => (child.size = 1)); + if (parent) { + const index = parent.children.findIndex((child) => child.id === targetNodeId); + if (index === -1) { + console.error("splitHorizontal (wrap): Target node not found in parent's children", targetNodeId); + return; + } + parent.children[index] = groupNode; + } else { + layoutState.rootNode = groupNode; + } + } + if (action.focused) { + layoutState.focusedNodeId = newNode.id; + } + layoutState.generation++; +} + +// ─── SPLIT VERTICAL ───────────────────────────────────────────────────────────── + +export function splitVertical(layoutState: LayoutTreeState, action: LayoutTreeSplitVerticalAction) { + const { targetNodeId, newNode, position } = action; + const targetNode = findNode(layoutState.rootNode, targetNodeId); + if (!targetNode) { + console.error("splitVertical: Target node not found", targetNodeId); + return; + } + + const parent = findParent(layoutState.rootNode, targetNodeId); + if (parent && parent.flexDirection === FlexDirection.Column) { + const index = parent.children.findIndex((child) => child.id === targetNodeId); + if (index === -1) { + console.error("splitVertical: Target node not found in parent's children", targetNodeId); + return; + } + const insertIndex = position === "before" ? index : index + 1; + // For vertical splits in an already vertical parent, splice directly. + parent.children.splice(insertIndex, 0, newNode); + parent.children.forEach((child) => (child.size = 1)); + } else { + // Wrap target node in a new vertical group. + // Create group node with an initial children array so that validation passes. + const groupNode = newLayoutNode(FlexDirection.Column, targetNode.size, [targetNode], undefined); + groupNode.children = position === "before" ? [newNode, targetNode] : [targetNode, newNode]; + groupNode.children.forEach((child) => (child.size = 1)); + if (parent) { + const index = parent.children.findIndex((child) => child.id === targetNodeId); + if (index === -1) { + console.error("splitVertical (wrap): Target node not found in parent's children", targetNodeId); + return; + } + parent.children[index] = groupNode; + } else { + layoutState.rootNode = groupNode; + } + } + if (action.focused) { + layoutState.focusedNodeId = newNode.id; + } + layoutState.generation++; +} diff --git a/frontend/layout/lib/types.ts b/frontend/layout/lib/types.ts index 6832601b92..1c8fa1f1dd 100644 --- a/frontend/layout/lib/types.ts +++ b/frontend/layout/lib/types.ts @@ -70,6 +70,9 @@ export enum LayoutTreeActionType { FocusNode = "focus", MagnifyNodeToggle = "magnify", ClearTree = "clear", + ReplaceNode = "replace", + SplitHorizontal = "splithorizontal", + SplitVertical = "splitvertical", } /** @@ -187,6 +190,33 @@ export interface LayoutTreeClearPendingAction extends LayoutTreeAction { type: LayoutTreeActionType.ClearPendingAction; } +// ReplaceNode: replace an existing node in place with a new one. +export interface LayoutTreeReplaceNodeAction extends LayoutTreeAction { + type: LayoutTreeActionType.ReplaceNode; + targetNodeId: string; + newNode: LayoutNode; +} + +// SplitHorizontal: split the current block horizontally. +// The "position" field indicates whether the new node should be inserted before (to the left) +// or after (to the right) of the target node. +export interface LayoutTreeSplitHorizontalAction extends LayoutTreeAction { + type: LayoutTreeActionType.SplitHorizontal; + targetNodeId: string; + newNode: LayoutNode; + position: "before" | "after"; + focused?: boolean; +} + +// SplitVertical: similar to split horizontal but along the vertical axis. +export interface LayoutTreeSplitVerticalAction extends LayoutTreeAction { + type: LayoutTreeActionType.SplitVertical; + targetNodeId: string; + newNode: LayoutNode; + position: "before" | "after"; + focused?: boolean; +} + /** * An operation to resize a node. */ diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index a690a60cec..d7270ae909 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -153,6 +153,8 @@ declare global { rtopts?: RuntimeOpts; magnified?: boolean; ephemeral?: boolean; + targetblockid?: string; + targetaction?: string; }; // wshrpc.CommandCreateSubBlockData @@ -480,6 +482,8 @@ declare global { focused: boolean; magnified: boolean; ephemeral: boolean; + targetblockid?: string; + position?: string; }; // waveobj.LayoutState diff --git a/pkg/waveobj/wtype.go b/pkg/waveobj/wtype.go index 89802acd98..1a8fe33c6f 100644 --- a/pkg/waveobj/wtype.go +++ b/pkg/waveobj/wtype.go @@ -202,13 +202,15 @@ func (t *Tab) GetBlockORefs() []ORef { } type LayoutActionData struct { - ActionType string `json:"actiontype"` - BlockId string `json:"blockid"` - NodeSize *uint `json:"nodesize,omitempty"` - IndexArr *[]int `json:"indexarr,omitempty"` - Focused bool `json:"focused"` - Magnified bool `json:"magnified"` - Ephemeral bool `json:"ephemeral"` + ActionType string `json:"actiontype"` + BlockId string `json:"blockid"` + NodeSize *uint `json:"nodesize,omitempty"` + IndexArr *[]int `json:"indexarr,omitempty"` + Focused bool `json:"focused"` + Magnified bool `json:"magnified"` + Ephemeral bool `json:"ephemeral"` + TargetBlockId string `json:"targetblockid,omitempty"` + Position string `json:"position,omitempty"` } type LeafOrderEntry struct { diff --git a/pkg/wcore/layout.go b/pkg/wcore/layout.go index 5be8d4e354..0d9a659176 100644 --- a/pkg/wcore/layout.go +++ b/pkg/wcore/layout.go @@ -14,10 +14,13 @@ import ( ) const ( - LayoutActionDataType_Insert = "insert" - LayoutActionDataType_InsertAtIndex = "insertatindex" - LayoutActionDataType_Remove = "delete" - LayoutActionDataType_ClearTree = "clear" + LayoutActionDataType_Insert = "insert" + LayoutActionDataType_InsertAtIndex = "insertatindex" + LayoutActionDataType_Remove = "delete" + LayoutActionDataType_ClearTree = "clear" + LayoutActionDataType_Replace = "replace" + LayoutActionDataType_SplitHorizontal = "splithorizontal" + LayoutActionDataType_SplitVertical = "splitvertical" ) type PortableLayout []struct { diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 2a74a9c63e..bca78a4ad5 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -41,6 +41,14 @@ const ( RpcType_Complex = "complex" // streaming request/response ) +const ( + CreateBlockAction_Replace = "replace" + CreateBlockAction_SplitUp = "splitup" + CreateBlockAction_SplitDown = "splitdown" + CreateBlockAction_SplitLeft = "splitleft" + CreateBlockAction_SplitRight = "splitright" +) + // TODO generate these constants from the interface const ( Command_Authenticate = "authenticate" // special @@ -337,11 +345,13 @@ type CommandResolveIdsRtnData struct { } type CommandCreateBlockData struct { - TabId string `json:"tabid" wshcontext:"TabId"` - BlockDef *waveobj.BlockDef `json:"blockdef"` - RtOpts *waveobj.RuntimeOpts `json:"rtopts,omitempty"` - Magnified bool `json:"magnified,omitempty"` - Ephemeral bool `json:"ephemeral,omitempty"` + TabId string `json:"tabid" wshcontext:"TabId"` + BlockDef *waveobj.BlockDef `json:"blockdef"` + RtOpts *waveobj.RuntimeOpts `json:"rtopts,omitempty"` + Magnified bool `json:"magnified,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` + TargetBlockId string `json:"targetblockid,omitempty"` + TargetAction string `json:"targetaction,omitempty"` // "replace", "splitright", "splitdown", "splitleft", "splitup" } type CommandCreateSubBlockData struct { diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 45426cbdd2..9db25050a7 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -208,13 +208,61 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command if err != nil { return nil, fmt.Errorf("error creating block: %w", err) } - err = wcore.QueueLayoutActionForTab(ctx, tabId, waveobj.LayoutActionData{ - ActionType: wcore.LayoutActionDataType_Insert, - BlockId: blockData.OID, - Magnified: data.Magnified, - Ephemeral: data.Ephemeral, - Focused: true, - }) + var layoutAction *waveobj.LayoutActionData + if data.TargetBlockId != "" { + switch data.TargetAction { + case "replace": + layoutAction = &waveobj.LayoutActionData{ + ActionType: wcore.LayoutActionDataType_Replace, + TargetBlockId: data.TargetBlockId, + BlockId: blockData.OID, + Focused: true, + } + err = wcore.DeleteBlock(ctx, data.TargetBlockId, false) + if err != nil { + return nil, fmt.Errorf("error deleting block (trying to do block replace): %w", err) + } + case "splitright": + layoutAction = &waveobj.LayoutActionData{ + ActionType: wcore.LayoutActionDataType_SplitHorizontal, + BlockId: blockData.OID, + TargetBlockId: data.TargetBlockId, + Position: "after", + } + case "splitleft": + layoutAction = &waveobj.LayoutActionData{ + ActionType: wcore.LayoutActionDataType_SplitHorizontal, + BlockId: blockData.OID, + TargetBlockId: data.TargetBlockId, + Position: "before", + } + case "splitup": + layoutAction = &waveobj.LayoutActionData{ + ActionType: wcore.LayoutActionDataType_SplitVertical, + BlockId: blockData.OID, + TargetBlockId: data.TargetBlockId, + Position: "before", + } + case "splitdown": + layoutAction = &waveobj.LayoutActionData{ + ActionType: wcore.LayoutActionDataType_SplitVertical, + BlockId: blockData.OID, + TargetBlockId: data.TargetBlockId, + Position: "after", + } + default: + return nil, fmt.Errorf("invalid target action: %s", data.TargetAction) + } + } else { + layoutAction = &waveobj.LayoutActionData{ + ActionType: wcore.LayoutActionDataType_Insert, + BlockId: blockData.OID, + Magnified: data.Magnified, + Ephemeral: data.Ephemeral, + Focused: true, + } + } + err = wcore.QueueLayoutActionForTab(ctx, tabId, *layoutAction) if err != nil { return nil, fmt.Errorf("error queuing layout action: %w", err) }