Skip to content

Commit 055b7b4

Browse files
committed
feat(desktop): add terminal tab context actions
1 parent 98e796a commit 055b7b4

1 file changed

Lines changed: 55 additions & 2 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-session.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
'use client'
22

3-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3+
import {
4+
type MouseEvent as ReactMouseEvent,
5+
useCallback,
6+
useEffect,
7+
useMemo,
8+
useRef,
9+
useState,
10+
} from 'react'
411
import { cn, TabStrip, type TabStripItem } from '@sim/emcn'
512
import { Loader, TerminalWindow } from '@sim/emcn/icons'
613
import { createLogger } from '@sim/logger'
@@ -24,6 +31,8 @@ import {
2431
writeToTerminal,
2532
} from '@/lib/terminal/transport'
2633
import { useMothershipResources } from '@/app/workspace/[workspaceId]/home/components/mothership-resources-context'
34+
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
35+
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
2736
import { useCopilotTerminalStore } from '@/stores/copilot-terminal/store'
2837

2938
const logger = createLogger('TerminalSession')
@@ -294,6 +303,16 @@ export function TerminalSession() {
294303
[tabs, activeTerminalId]
295304
)
296305

306+
const [contextTerminalId, setContextTerminalId] = useState<string | null>(null)
307+
const {
308+
isOpen: isContextMenuOpen,
309+
position: contextMenuPosition,
310+
menuRef: contextMenuRef,
311+
handleContextMenu,
312+
closeMenu: closeContextMenu,
313+
} = useContextMenu()
314+
const contextTab = tabs.find((tab) => tab.terminalId === contextTerminalId)
315+
297316
const handleNew = useCallback(() => {
298317
void openTerminal()
299318
}, [])
@@ -304,17 +323,51 @@ export function TerminalSession() {
304323
void closeTerminal(terminalId)
305324
}, [])
306325

326+
// A duplicate is a new shell in the same directory, not a copy of the
327+
// session: scrollback and whatever is running belong to the original pty.
328+
const handleDuplicate = useCallback((cwd: string | null) => {
329+
void openTerminal(cwd ?? undefined)
330+
}, [])
331+
332+
const openTabContextMenu = useCallback(
333+
(event: ReactMouseEvent<HTMLDivElement>, terminalId: string) => {
334+
window.getSelection()?.removeAllRanges()
335+
setContextTerminalId(terminalId)
336+
handleContextMenu(event)
337+
},
338+
[handleContextMenu]
339+
)
340+
307341
return (
308342
<div className='flex h-full flex-col overflow-hidden bg-[var(--bg)]'>
309343
{tabs.length > 0 && (
310344
<TabStrip
311345
tabs={items}
312346
onSelect={handleSwitch}
313347
onNew={handleNew}
348+
onTabContextMenu={openTabContextMenu}
314349
maxTabs={MAX_TERMINALS}
315350
newTabLabel='New terminal'
316351
{...(tabs.length > 1 ? { onClose: handleClose } : {})}
317-
/>
352+
>
353+
<ContextMenu
354+
isOpen={isContextMenuOpen && Boolean(contextTab)}
355+
position={contextMenuPosition}
356+
menuRef={contextMenuRef}
357+
onClose={closeContextMenu}
358+
onDuplicate={contextTab ? () => handleDuplicate(contextTab.cwd) : undefined}
359+
// The last terminal has no close affordance in the strip either:
360+
// closing it would leave the panel with nothing to show.
361+
{...(contextTab && tabs.length > 1
362+
? { onCloseTab: () => handleClose(contextTab.terminalId), showCloseTab: true }
363+
: {})}
364+
onDelete={() => {}}
365+
showRename={false}
366+
showDuplicate={Boolean(contextTab)}
367+
disableDuplicate={tabs.length >= MAX_TERMINALS}
368+
showDelete={false}
369+
/>
370+
</TabStrip>
318371
)}
319372

320373
<div className='relative min-h-0 flex-1'>

0 commit comments

Comments
 (0)