Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/treebeard/src/bun/services/launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ describe('launcher service', () => {
)
})

it('launches ghostty with open -a and path argument', async () => {
it('launches ghostty with AppleScript and sets tab title', async () => {
const spawn = setBunSpawnQueue([{ stdout: '' }])

await launchGhostty('/repo/worktree')
await launchGhostty('/Users/user/projects/node-commons/this-is-the-worktree')

expect(spawn).toHaveBeenCalledWith(
['open', '-a', 'Ghostty.app', '/repo/worktree'],
['/usr/bin/osascript', '-e', expect.stringContaining('tell application "Ghostty"')],
expect.objectContaining({ stdout: 'ignore', stderr: 'ignore' })
)
})
Expand Down
35 changes: 26 additions & 9 deletions packages/treebeard/src/bun/services/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,32 @@ export async function launchIde(ideId: IdeId, worktreePath: string): Promise<voi
}

export async function launchGhostty(worktreePath: string): Promise<void> {
// Pass the path as a file argument so open(1) forwards it to the running
// instance via application:openFile:, which Ghostty maps to a new tab/window
// in the correct directory without spawning a second process.
const env = await getShellEnv()
Bun.spawn(['open', '-a', 'Ghostty.app', worktreePath], {
stdout: 'ignore',
stderr: 'ignore',
env
})
// Use AppleScript to switch to an existing Ghostty tab for this worktree,
// or open a new tab in the correct directory if none exists.
// This ensures proper tab naming and tab management like the opencode launcher.
const pathParts = worktreePath.split('/')
const worktreeName = pathParts.pop() || ''
const projectName = pathParts.pop() || ''
const tabTitle = `${projectName} / ${worktreeName}`
const script = `
tell application "Ghostty"
set targetPath to "${worktreePath}"
set targetWindow to window 1
repeat with t in every tab of targetWindow
set term to focused terminal of t
if working directory of term is targetPath then
select tab t
focus term
return
end if
end repeat
set cfg to new surface configuration
set initial working directory of cfg to targetPath
set newTab to new tab in targetWindow with configuration cfg
perform action "set_tab_title:${tabTitle}" on (focused terminal of newTab)
end tell
`
Bun.spawn(['/usr/bin/osascript', '-e', script], { stdout: 'ignore', stderr: 'ignore' })
}

export async function launchOpencode(worktreePath: string): Promise<void> {
Expand Down
17 changes: 0 additions & 17 deletions packages/treebeard/src/components/LaunchButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'
import { ActionIcon, Group, Tooltip } from '@mantine/core'
import { IconGhost } from '@tabler/icons-react'
import { IdeIcon } from './IdeIcon'
import { OpencodeIcon } from './OpencodeIcon'
import { IDE_REGISTRY } from '../shared/ide-registry'
import { rpc } from '../rpc'
import type { IdeId } from '../shared/types'
Expand All @@ -14,11 +13,6 @@ interface LaunchButtonsProps {

export function LaunchButtons({ worktreePath, defaultIde }: LaunchButtonsProps) {
const ide = IDE_REGISTRY[defaultIde]
const [opencodePath, setOpencodePath] = useState<string | null>(null)

useEffect(() => {
rpc().request['system:opencodePath']({}).then(setOpencodePath).catch(() => setOpencodePath(null))
}, [])

const handleIde = async () => {
await rpc().request['launch:ide']({ ideId: defaultIde, worktreePath })
Expand All @@ -28,10 +22,6 @@ export function LaunchButtons({ worktreePath, defaultIde }: LaunchButtonsProps)
await rpc().request['launch:ghostty']({ worktreePath })
}

const handleOpencode = async () => {
await rpc().request['launch:opencode']({ worktreePath })
}

return (
<Group gap={4}>
<Tooltip label={`Open in ${ide.label}`}>
Expand All @@ -44,13 +34,6 @@ export function LaunchButtons({ worktreePath, defaultIde }: LaunchButtonsProps)
<IconGhost size={16} />
</ActionIcon>
</Tooltip>
{opencodePath && (
<Tooltip label="Open in OpenCode">
<ActionIcon variant="subtle" size="sm" onClick={handleOpencode}>
<OpencodeIcon size={16} />
</ActionIcon>
</Tooltip>
)}
</Group>
)
}
Loading