Fix macro splits to use tab groups

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-05-19 14:28:42 -07:00
co-authored by Orca
parent 7dfda9b91d
commit 2dfc299714
5 changed files with 58 additions and 37 deletions
@@ -162,8 +162,8 @@ export function ExperimentalPane({
<div className="min-w-0 shrink space-y-1.5">
<Label>Terminal Macros</Label>
<p className="text-xs text-muted-foreground">
Define named terminal launch presets that open a new tab, optionally create a split
right or down, and seed the new terminal with startup text. This remains
Define named terminal launch presets that open a new tab in the current tab group or
a new tab split, then seed the new terminal with startup text. This remains
experimental and appears in the tab-bar new-tab menu only when enabled.
</p>
</div>
@@ -21,10 +21,10 @@ type EditorState =
function layoutLabel(layout: TerminalMacro['layout']): string {
if (layout === 'split-right') {
return 'Split Right'
return 'Tab Split Right'
}
if (layout === 'split-down') {
return 'Split Down'
return 'Tab Split Down'
}
return 'Tab'
}
@@ -54,7 +54,7 @@ export function TerminalMacrosSection({
<div className="space-y-1">
<Label>Saved Macros</Label>
<p className="text-xs text-muted-foreground">
Each macro opens a named tab and can seed one startup split.
Each macro opens a named tab in the current tab group or a new tab split.
</p>
</div>
<Button
@@ -76,7 +76,7 @@ export function TerminalMacroDialog({
{mode === 'edit' ? 'Edit Macro' : 'Add Macro'}
</DialogTitle>
<DialogDescription className="text-xs">
Macros open a named terminal tab and optionally create one startup split.
Macros open a named terminal tab and can place it in a tab split.
</DialogDescription>
</DialogHeader>
@@ -108,8 +108,8 @@ export function TerminalMacroDialog({
className="justify-start"
>
<ToggleGroupItem value="tab">Tab</ToggleGroupItem>
<ToggleGroupItem value="split-right">Split Right</ToggleGroupItem>
<ToggleGroupItem value="split-down">Split Down</ToggleGroupItem>
<ToggleGroupItem value="split-right">Tab Split Right</ToggleGroupItem>
<ToggleGroupItem value="split-down">Tab Split Down</ToggleGroupItem>
</ToggleGroup>
</div>
@@ -124,24 +124,15 @@ export function TerminalMacroDialog({
rows={5}
className="min-h-28 w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm font-mono shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
/>
{targetsSplitPane ? (
<p className="text-xs text-muted-foreground">
This startup text runs only in the newly created split pane. The original pane is
left alone.
</p>
) : (
{!targetsSplitPane && (
<p className="text-xs text-muted-foreground">
Optional startup text for the new tab.
</p>
)}
<MacroAppendEnterToggle
checked={draft.appendEnter !== false}
label={targetsSplitPane ? 'Run new pane startup text' : 'Run tab startup text'}
description={
targetsSplitPane
? 'Press Enter after sending the new split pane text.'
: 'Press Enter after sending the tab startup text.'
}
label="Run startup command"
description="Press Enter after this text is in the terminal."
onToggle={() =>
setDraft((current) => ({ ...current, appendEnter: !current.appendEnter }))
}
@@ -2,11 +2,18 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const state = {
createTab: vi.fn(),
createEmptySplitGroup: vi.fn(),
setTabCustomTitle: vi.fn(),
queueTabStartupCommand: vi.fn(),
queueTabSetupSplit: vi.fn(),
setActiveTabType: vi.fn(),
setTabBarOrder: vi.fn(),
activeGroupIdByWorktree: {
'wt-1': 'group-1'
},
groupsByWorktree: {
'wt-1': [{ id: 'group-1' }]
},
tabsByWorktree: {
'wt-1': [{ id: 'existing-tab' }, { id: 'tab-1' }]
},
@@ -29,6 +36,7 @@ describe('launchTerminalMacro', () => {
beforeEach(() => {
vi.clearAllMocks()
state.createTab.mockReturnValue({ id: 'tab-1', title: 'Terminal 1' })
state.createEmptySplitGroup.mockReturnValue('group-2')
})
it('creates a named tab and queues startup for tab macros', async () => {
@@ -62,7 +70,7 @@ describe('launchTerminalMacro', () => {
expect(result).toEqual({ tabId: 'tab-1' })
})
it('queues an initial split and sends startup only to the new split pane', async () => {
it('creates a tab group split and queues startup on the new tab', async () => {
const { launchTerminalMacro } = await import('./launch-terminal-macro')
launchTerminalMacro({
@@ -76,11 +84,12 @@ describe('launchTerminalMacro', () => {
worktreeId: 'wt-1'
})
expect(state.queueTabStartupCommand).not.toHaveBeenCalled()
expect(state.queueTabSetupSplit).toHaveBeenCalledWith('tab-1', {
direction: 'vertical',
expect(state.createEmptySplitGroup).toHaveBeenCalledWith('wt-1', 'group-1', 'right')
expect(state.createTab).toHaveBeenCalledWith('wt-1', 'group-2')
expect(state.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', {
command: 'npm run dev\r'
})
expect(state.queueTabSetupSplit).not.toHaveBeenCalled()
})
it('returns null for blank names', async () => {
+34 -13
View File
@@ -20,27 +20,25 @@ export function launchTerminalMacro({
return null
}
const tab = store.createTab(worktreeId, groupId)
const targetGroupId =
macro.layout === 'split-right' || macro.layout === 'split-down'
? resolveTerminalMacroSplitGroupId({
worktreeId,
groupId,
direction: macro.layout === 'split-right' ? 'right' : 'down'
})
: groupId
const tab = store.createTab(worktreeId, targetGroupId)
store.setTabCustomTitle(tab.id, name)
const command = macro.command.trimEnd()
if (command && macro.layout === 'tab') {
if (command) {
store.queueTabStartupCommand(tab.id, {
command: buildTerminalMacroInput(command, macro.appendEnter !== false)
})
}
if (macro.layout === 'split-right' || macro.layout === 'split-down') {
store.queueTabSetupSplit(tab.id, {
direction: macro.layout === 'split-right' ? 'vertical' : 'horizontal',
...(command
? {
command: buildTerminalMacroInput(command, macro.appendEnter !== false)
}
: {})
})
}
// Why: macro launches should surface the new terminal immediately even when
// the user currently has an editor/browser active in the same worktree.
store.setActiveTabType('terminal')
@@ -63,3 +61,26 @@ export function launchTerminalMacro({
return { tabId: tab.id }
}
function resolveTerminalMacroSplitGroupId({
worktreeId,
groupId,
direction
}: {
worktreeId: string
groupId?: string
direction: 'right' | 'down'
}): string | undefined {
const store = useAppStore.getState()
const sourceGroupId =
groupId ??
store.activeGroupIdByWorktree[worktreeId] ??
store.groupsByWorktree[worktreeId]?.[0]?.id
if (!sourceGroupId) {
return undefined
}
// Why: macro "split right/down" means an Orca tab-group split, not an
// xterm pane split. Keeping it here avoids remounting the source terminal.
return store.createEmptySplitGroup(worktreeId, sourceGroupId, direction) ?? undefined
}