Preserve split-terminal setup milestone (#4546)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson
2026-06-03 17:11:29 -07:00
committed by GitHub
co-authored by Orca
parent 6ba4bf4fd4
commit 065c620c10
14 changed files with 339 additions and 86 deletions
+2 -1
View File
@@ -34,6 +34,7 @@ import {
import { isIntentionalAppRestartInProgress } from '@/lib/updater-beforeunload'
import EditorAutosaveController from './editor/EditorAutosaveController'
import type { Tab, TabContentType, TabGroupLayoutNode } from '../../../shared/types'
import { hasFeatureInteraction } from '../../../shared/feature-interactions'
import BrowserPane from './browser-pane/BrowserPane'
import BrowserPaneOverlayLayer from './browser-pane/BrowserPaneOverlayLayer'
import { useBrowserAutomationVisibilityForAny } from './browser-pane/browser-automation-visibility'
@@ -283,7 +284,7 @@ function Terminal(): React.JSX.Element | null {
: ''
const activeContextualTourId = useAppStore((s) => s.activeContextualTourId)
const hasSplitTerminalPane = useAppStore((s) =>
Boolean(s.featureInteractions['terminal-pane-split']?.interactionCount)
hasFeatureInteraction(s.featureInteractions, 'terminal-pane-split')
)
useContextualTour(
@@ -115,7 +115,7 @@ describe('getFeatureWallSetupProgress', () => {
expect(getFirstIncompleteFeatureWallSetupStepId(progress.stepDone)).toBe('split-terminal')
})
it('does not mark the step complete from split-pane interaction count alone', () => {
it('marks the step complete from durable terminal-pane split interaction state', () => {
const progress = getFeatureWallSetupProgress(
makeInput({
featureInteractions: {
@@ -124,6 +124,30 @@ describe('getFeatureWallSetupProgress', () => {
})
)
expect(progress.stepDone['split-terminal']).toBe(true)
})
it('does not mark the step complete from malformed durable terminal-pane split state', () => {
const progress = getFeatureWallSetupProgress(
makeInput({
featureInteractions: {
'terminal-pane-split': { firstInteractedAt: Number.NaN, interactionCount: 1 }
}
})
)
expect(progress.stepDone['split-terminal']).toBe(false)
})
it('does not mark the step complete from generic pane interaction state', () => {
const progress = getFeatureWallSetupProgress(
makeInput({
featureInteractions: {
'terminal-panes': { firstInteractedAt: 1_700_000_000_000, interactionCount: 1 }
}
})
)
expect(progress.stepDone['split-terminal']).toBe(false)
})
@@ -141,7 +165,7 @@ describe('getFeatureWallSetupProgress', () => {
expect(progress.stepDone['split-terminal']).toBe(false)
})
it('marks the step complete once a worktree terminal is split into two panes', () => {
it('does not mark the step complete from a live split layout without durable state', () => {
const progress = getFeatureWallSetupProgress(
makeInput({
worktreesByRepo: { 'repo-1': [makeWorktree('worktree-1')] },
@@ -152,7 +176,37 @@ describe('getFeatureWallSetupProgress', () => {
})
)
expect(progress.stepDone['split-terminal']).toBe(true)
expect(progress.stepDone['split-terminal']).toBe(false)
})
it('keeps the step complete after the split tab closes from durable state', () => {
const withSplit = getFeatureWallSetupProgress(
makeInput({
featureInteractions: {
'terminal-pane-split': { firstInteractedAt: 1_700_000_000_000, interactionCount: 1 }
},
worktreesByRepo: { 'repo-1': [makeWorktree('worktree-1')] },
tabsByWorktree: {
'worktree-1': [{ id: 'tab-1', title: 'Terminal' }] as never
},
terminalLayoutsByTabId: { 'tab-1': makeSplitLayout() }
})
)
expect(withSplit.stepDone['split-terminal']).toBe(true)
const afterClosingSplitTab = getFeatureWallSetupProgress(
makeInput({
featureInteractions: {
'terminal-pane-split': { firstInteractedAt: 1_700_000_000_000, interactionCount: 1 }
},
worktreesByRepo: { 'repo-1': [makeWorktree('worktree-1')] },
tabsByWorktree: { 'worktree-1': [] },
terminalLayoutsByTabId: {}
})
)
expect(afterClosingSplitTab.stepDone['split-terminal']).toBe(true)
})
it('ignores split layouts for tabs that do not belong to a known worktree', () => {
@@ -1,4 +1,5 @@
import type { FeatureInteractionState } from '../../../../shared/feature-interactions'
import { hasFeatureInteraction } from '../../../../shared/feature-interactions'
import {
FEATURE_WALL_SETUP_STEPS,
type FeatureWallSetupStepId
@@ -6,7 +7,6 @@ import {
import type {
GlobalSettings,
TerminalLayoutSnapshot,
TerminalPaneLayoutNode,
TerminalTab,
Worktree
} from '../../../../shared/types'
@@ -35,30 +35,6 @@ export type FeatureWallSetupProgress = {
coreTotal: number
}
function isSplitLayout(node: TerminalPaneLayoutNode | null | undefined): boolean {
// A split node means the tab holds 2+ panes, regardless of what runs in them.
return Boolean(node) && node!.type === 'split'
}
function hasSplitTerminalInAnyWorktree(input: FeatureWallSetupProgressInput): boolean {
const validWorktreeIds = new Set(
Object.values(input.worktreesByRepo)
.flat()
.map((worktree) => worktree.id)
)
for (const [worktreeId, tabs] of Object.entries(input.tabsByWorktree)) {
if (!validWorktreeIds.has(worktreeId)) {
continue
}
for (const tab of tabs) {
if (isSplitLayout(input.terminalLayoutsByTabId[tab.id]?.root)) {
return true
}
}
}
return false
}
function countAvailableNonMainWorktrees(worktreesByRepo: Record<string, Worktree[]>): number {
// Why: imported git worktrees count as real parallel-work capacity, but
// partially hydrated placeholders can appear before a worktree path is known.
@@ -87,7 +63,7 @@ export function getFeatureWallSetupProgress(
notifications:
input.settings?.notifications.enabled === true &&
input.settings.notifications.agentTaskComplete === true,
'split-terminal': hasSplitTerminalInAnyWorktree(input),
'split-terminal': hasFeatureInteraction(input.featureInteractions, 'terminal-pane-split'),
'two-worktrees': countAvailableNonMainWorktrees(input.worktreesByRepo) >= 1,
'task-sources': input.hasConnectedTaskSource,
'agent-capabilities': agentCapabilitiesDone,
@@ -20,6 +20,7 @@ const mocks = vi.hoisted(() => ({
isWebRuntimeSessionActive: vi.fn(() => false),
openFile: vi.fn(),
pinFile: vi.fn(),
recordFeatureInteraction: vi.fn(),
setActiveBrowserTab: vi.fn(),
setActiveFile: vi.fn(),
setActiveTab: vi.fn(),
@@ -143,6 +144,7 @@ function resetStore(): void {
focusGroup: mocks.focusGroup,
openFile: mocks.openFile,
pinFile: mocks.pinFile,
recordFeatureInteraction: mocks.recordFeatureInteraction,
setActiveBrowserTab: mocks.setActiveBrowserTab,
setActiveFile: mocks.setActiveFile,
setActiveTab: mocks.setActiveTab,
@@ -197,4 +199,19 @@ describe('useTabGroupWorkspaceModel terminal activation focus', () => {
expect(event.type).toBe(TOGGLE_TERMINAL_PANE_EXPAND_EVENT)
expect(event.detail).toEqual({ tabId: 'terminal-1' })
})
it('records terminal split completion when splitting a single terminal tab group', async () => {
mocks.createEmptySplitGroup.mockReturnValue('group-2')
mocks.createTab.mockReturnValue({ id: 'terminal-2' })
const { useTabGroupWorkspaceModel } = await import('./useTabGroupWorkspaceModel')
const model = useTabGroupWorkspaceModel({ groupId: 'group-1', worktreeId: 'wt-1' })
model.commands.createSplitGroup('right')
expect(mocks.createEmptySplitGroup).toHaveBeenCalledWith('wt-1', 'group-1', 'right')
expect(mocks.createTab).toHaveBeenCalledWith('wt-1', 'group-2')
expect(mocks.recordFeatureInteraction).toHaveBeenCalledWith('terminal-pane-split')
expect(mocks.setActiveTab).toHaveBeenCalledWith('terminal-2')
expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal')
})
})
@@ -25,6 +25,13 @@ import {
} from '../../runtime/web-runtime-session'
import { openTabBarEntry, type TabCreateEntryArgs } from '../tab-bar/tab-create-entry-action'
export function recordTerminalTabGroupSplit(createdTerminal: TerminalTab | null | undefined): void {
if (!createdTerminal) {
return
}
useAppStore.getState().recordFeatureInteraction('terminal-pane-split')
}
export type GroupEditorItem = OpenFile & { tabId: string }
export type GroupBrowserItem = BrowserTabState & { tabId: string }
@@ -420,6 +427,7 @@ export function useTabGroupWorkspaceModel({
return
}
const terminal = createTab(worktreeId, newGroupId)
recordTerminalTabGroupSplit(terminal)
setActiveTab(terminal.id)
setActiveTabType('terminal')
return
@@ -388,7 +388,6 @@ export default function TerminalPane({
const clearWorktreeUnread = useAppStore((store) => store.clearWorktreeUnread)
const clearTerminalTabUnread = useAppStore((store) => store.clearTerminalTabUnread)
const clearTerminalPaneUnread = useAppStore((store) => store.clearTerminalPaneUnread)
const recordFeatureInteraction = useAppStore((store) => store.recordFeatureInteraction)
const openSpacePage = useAppStore((store) => store.openSpacePage)
const refreshWorkspaceSpace = useAppStore((store) => store.refreshWorkspaceSpace)
const settings = useAppStore((store) => store.settings)
@@ -408,10 +407,6 @@ export default function TerminalPane({
() => useAppStore.getState().pendingIssueCommandSplitByTabId[tabId]
)
const consumeTabIssueCommandSplit = useAppStore((store) => store.consumeTabIssueCommandSplit)
const handleSplitPaneCommand = useCallback(() => {
recordFeatureInteraction('terminal-pane-split')
}, [recordFeatureInteraction])
useEffect(() => {
if (startup) {
consumeTabStartupCommand(tabId)
@@ -1063,7 +1058,6 @@ export default function TerminalPane({
setSearchOpen,
onSearchSelectedText: handleSearchSelectedText,
onRequestClosePane: handleRequestClosePane,
onSplitPaneCommand: handleSplitPaneCommand,
searchOpenRef,
searchStateRef,
macOptionAsAltRef,
@@ -1588,7 +1582,6 @@ export default function TerminalPane({
fallbackCwd: cwd ?? '',
toggleExpandPane,
onRequestClosePane: handleRequestClosePane,
onSplitPaneCommand: handleSplitPaneCommand,
onSetTitle: handleStartRename,
onPasteError: setTerminalError,
onAgentSessionForkReady: setAgentSessionFork,
@@ -17,9 +17,19 @@ import { keyboardEventBelongsToScope } from './terminal-keyboard-scope'
import { normalizeSelectedTextForFileSearch } from '@/lib/file-search-selection'
import { splitWebRuntimeTerminal } from '@/runtime/web-runtime-session'
import { handleEmptyFloatingWorkspacePanelCloseShortcut } from '@/lib/floating-workspace-terminal-actions'
import { trackTerminalPaneSplit } from '@/lib/feature-education-telemetry'
import { recordCreatedTerminalPaneSplit } from './terminal-pane-split-completion'
import { useAppStore } from '@/store'
export function recordKeyboardCreatedTerminalPaneSplit(
createdPane: unknown,
args: {
source: 'contextual_tour' | 'keyboard'
direction: 'vertical' | 'horizontal'
}
): boolean {
return recordCreatedTerminalPaneSplit(createdPane, args)
}
function isEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) {
return false
@@ -110,7 +120,6 @@ type KeyboardHandlersDeps = {
setSearchOpen: React.Dispatch<React.SetStateAction<boolean>>
onSearchSelectedText: (text: string) => void
onRequestClosePane: (paneId: number) => void
onSplitPaneCommand?: () => void
searchOpenRef: React.RefObject<boolean>
searchStateRef: React.RefObject<SearchState>
macOptionAsAltRef: React.RefObject<MacOptionAsAlt>
@@ -134,7 +143,6 @@ export function useTerminalKeyboardShortcuts({
setSearchOpen,
onSearchSelectedText,
onRequestClosePane,
onSplitPaneCommand,
searchOpenRef,
searchStateRef,
macOptionAsAltRef,
@@ -376,7 +384,6 @@ export function useTerminalKeyboardShortcuts({
if (!pane) {
return
}
onSplitPaneCommand?.()
const ptyId = paneTransportsRef.current.get(pane.id)?.getPtyId() ?? null
const telemetrySource = getKeyboardSplitTelemetrySource()
if (splitWebRuntimeTerminal(ptyId, action.direction, telemetrySource)) {
@@ -389,12 +396,10 @@ export function useTerminalKeyboardShortcuts({
const cached = paneCwdRef.current.get(pane.id)
if (cached?.confirmed && cached.cwd) {
const createdPane = manager.splitPane(pane.id, action.direction, { cwd: cached.cwd })
if (createdPane) {
trackTerminalPaneSplit({
source: telemetrySource,
direction: action.direction
})
}
recordKeyboardCreatedTerminalPaneSplit(createdPane, {
source: telemetrySource,
direction: action.direction
})
return
}
const paneIdAtDispatch = pane.id
@@ -409,12 +414,10 @@ export function useTerminalKeyboardShortcuts({
const createdPane = managerRef.current?.splitPane(paneIdAtDispatch, directionAtDispatch, {
cwd
})
if (createdPane) {
trackTerminalPaneSplit({
source: telemetrySource,
direction: directionAtDispatch
})
}
recordKeyboardCreatedTerminalPaneSplit(createdPane, {
source: telemetrySource,
direction: directionAtDispatch
})
})()
}
}
@@ -443,7 +446,6 @@ export function useTerminalKeyboardShortcuts({
setSearchOpen,
onSearchSelectedText,
onRequestClosePane,
onSplitPaneCommand,
searchOpenRef,
searchStateRef,
macOptionAsAltRef,
@@ -0,0 +1,72 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { recordCreatedTerminalPaneSplit } from './terminal-pane-split-completion'
const mocks = vi.hoisted(() => ({
recordFeatureInteraction: vi.fn(),
trackTerminalPaneSplit: vi.fn()
}))
vi.mock('@/store', () => ({
useAppStore: {
getState: () => ({
recordFeatureInteraction: mocks.recordFeatureInteraction
})
}
}))
vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
describe('recordCreatedTerminalPaneSplit', () => {
beforeEach(() => {
mocks.recordFeatureInteraction.mockReset()
mocks.trackTerminalPaneSplit.mockReset()
})
it('does not record durable split completion when no pane was created', () => {
expect(
recordCreatedTerminalPaneSplit(null, {
source: 'keyboard',
direction: 'vertical'
})
).toBe(false)
expect(mocks.recordFeatureInteraction).not.toHaveBeenCalled()
expect(mocks.trackTerminalPaneSplit).not.toHaveBeenCalled()
})
it('records durable split completion and telemetry after a pane is created', () => {
expect(
recordCreatedTerminalPaneSplit(
{ id: 2 },
{
source: 'context_menu',
direction: 'horizontal'
}
)
).toBe(true)
expect(mocks.recordFeatureInteraction).toHaveBeenCalledWith('terminal-pane-split')
expect(mocks.trackTerminalPaneSplit).toHaveBeenCalledWith({
source: 'context_menu',
direction: 'horizontal'
})
})
it('keeps durable split completion when mirrored runtime telemetry is suppressed', () => {
expect(
recordCreatedTerminalPaneSplit(
{ id: 2 },
{
source: 'command',
direction: 'vertical',
telemetrySuppressed: true
}
)
).toBe(true)
expect(mocks.recordFeatureInteraction).toHaveBeenCalledWith('terminal-pane-split')
expect(mocks.trackTerminalPaneSplit).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,26 @@
import { trackTerminalPaneSplit } from '@/lib/feature-education-telemetry'
import { useAppStore } from '@/store'
import type { TerminalPaneSplitSource } from '../../../../shared/feature-education-telemetry'
export type TerminalPaneSplitCompletion = {
source: TerminalPaneSplitSource
direction: 'vertical' | 'horizontal'
telemetrySuppressed?: boolean
}
export function recordCreatedTerminalPaneSplit(
createdPane: unknown,
completion: TerminalPaneSplitCompletion
): boolean {
if (!createdPane) {
return false
}
useAppStore.getState().recordFeatureInteraction('terminal-pane-split')
if (!completion.telemetrySuppressed) {
trackTerminalPaneSplit({
source: completion.source,
direction: completion.direction
})
}
return true
}
@@ -0,0 +1,93 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { recordKeyboardCreatedTerminalPaneSplit } from './keyboard-handlers'
import { recordContextMenuCreatedTerminalPaneSplit } from './use-terminal-pane-context-menu'
import { recordRuntimeCreatedTerminalPaneSplit } from './use-terminal-pane-lifecycle'
const mocks = vi.hoisted(() => ({
recordFeatureInteraction: vi.fn(),
trackTerminalPaneSplit: vi.fn()
}))
vi.mock('@/store', () => ({
useAppStore: {
getState: () => ({
activeContextualTourId: null,
recordFeatureInteraction: mocks.recordFeatureInteraction
})
}
}))
vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
describe('terminal split writer paths', () => {
beforeEach(() => {
mocks.recordFeatureInteraction.mockReset()
mocks.trackTerminalPaneSplit.mockReset()
})
it('does not record keyboard split completion when the local split fails', () => {
expect(
recordKeyboardCreatedTerminalPaneSplit(null, {
source: 'keyboard',
direction: 'vertical'
})
).toBe(false)
expect(mocks.recordFeatureInteraction).not.toHaveBeenCalled()
expect(mocks.trackTerminalPaneSplit).not.toHaveBeenCalled()
})
it('records context-menu split completion after the local split succeeds', () => {
expect(
recordContextMenuCreatedTerminalPaneSplit(
{ id: 2 },
{
source: 'context_menu',
direction: 'horizontal'
}
)
).toBe(true)
expect(mocks.recordFeatureInteraction).toHaveBeenCalledWith('terminal-pane-split')
expect(mocks.trackTerminalPaneSplit).toHaveBeenCalledWith({
source: 'context_menu',
direction: 'horizontal'
})
})
it('records runtime split completion after SPLIT_TERMINAL_PANE_EVENT creates a pane', () => {
expect(
recordRuntimeCreatedTerminalPaneSplit(
{ id: 2 },
{
source: 'command',
direction: 'vertical'
}
)
).toBe(true)
expect(mocks.recordFeatureInteraction).toHaveBeenCalledWith('terminal-pane-split')
expect(mocks.trackTerminalPaneSplit).toHaveBeenCalledWith({
source: 'command',
direction: 'vertical'
})
})
it('keeps runtime split completion when mirrored telemetry is suppressed', () => {
expect(
recordRuntimeCreatedTerminalPaneSplit(
{ id: 2 },
{
source: 'command',
direction: 'horizontal',
telemetrySuppressed: true
}
)
).toBe(true)
expect(mocks.recordFeatureInteraction).toHaveBeenCalledWith('terminal-pane-split')
expect(mocks.trackTerminalPaneSplit).not.toHaveBeenCalled()
})
})
@@ -22,11 +22,21 @@ import {
prepareAgentSessionForkFromPane,
type PreparedAgentSessionFork
} from './terminal-agent-session-fork'
import { trackTerminalPaneSplit } from '@/lib/feature-education-telemetry'
import { recordCreatedTerminalPaneSplit } from './terminal-pane-split-completion'
import { useAppStore } from '@/store'
const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus'
export function recordContextMenuCreatedTerminalPaneSplit(
createdPane: unknown,
args: {
source: 'contextual_tour' | 'context_menu'
direction: 'vertical' | 'horizontal'
}
): boolean {
return recordCreatedTerminalPaneSplit(createdPane, args)
}
type UseTerminalPaneContextMenuDeps = {
managerRef: React.RefObject<PaneManager | null>
paneTransportsRef: React.RefObject<Map<number, PtyTransport>>
@@ -37,7 +47,6 @@ type UseTerminalPaneContextMenuDeps = {
fallbackCwd: string
toggleExpandPane: (paneId: number) => void
onRequestClosePane: (paneId: number) => void
onSplitPaneCommand?: () => void
onSetTitle: (paneId: number) => void
onPasteError: (message: string) => void
onAgentSessionForkReady: (fork: PreparedAgentSessionFork) => void
@@ -76,7 +85,6 @@ export function useTerminalPaneContextMenu({
fallbackCwd,
toggleExpandPane,
onRequestClosePane,
onSplitPaneCommand,
onSetTitle,
onPasteError,
onAgentSessionForkReady,
@@ -176,7 +184,6 @@ export function useTerminalPaneContextMenu({
if (!pane) {
return
}
onSplitPaneCommand?.()
const ptyId = paneTransportsRef.current.get(pane.id)?.getPtyId() ?? null
if (splitWebRuntimeTerminal(ptyId, direction, source)) {
return
@@ -184,9 +191,7 @@ export function useTerminalPaneContextMenu({
const cached = paneCwdRef.current.get(pane.id)
if (cached?.confirmed && cached.cwd) {
const createdPane = managerRef.current?.splitPane(pane.id, direction, { cwd: cached.cwd })
if (createdPane) {
trackTerminalPaneSplit({ source, direction })
}
recordContextMenuCreatedTerminalPaneSplit(createdPane, { source, direction })
return
}
const paneId = pane.id
@@ -198,12 +203,10 @@ export function useTerminalPaneContextMenu({
fallbackCwd
})
const createdPane = managerRef.current?.splitPane(paneId, direction, { cwd })
if (createdPane) {
trackTerminalPaneSplit({ source, direction })
}
recordContextMenuCreatedTerminalPaneSplit(createdPane, { source, direction })
})()
},
[fallbackCwd, managerRef, onSplitPaneCommand, paneCwdRef, paneTransportsRef, resolveMenuPane]
[fallbackCwd, managerRef, paneCwdRef, paneTransportsRef, resolveMenuPane]
)
const onSplitRight = (): void => splitWithInheritedCwd('vertical')
@@ -3,7 +3,6 @@ import { useEffect, useRef } from 'react'
import type { IDisposable, Terminal } from '@xterm/xterm'
import type { ParsedAgentStatusPayload } from '../../../../shared/agent-status-types'
import { PaneManager } from '@/lib/pane-manager/pane-manager'
import { trackTerminalPaneSplit } from '@/lib/feature-education-telemetry'
import { consumePendingWebRuntimeSplitMirrorTelemetry } from '@/runtime/web-runtime-session'
import { resolveTerminalCursorInactiveStyle } from '@/lib/pane-manager/pane-terminal-options'
import { buildWindowsPtyCompatibilityOptions } from '@/lib/pane-manager/windows-pty-compatibility'
@@ -23,6 +22,7 @@ import type {
TerminalTab,
TerminalLayoutSnapshot
} from '../../../../shared/types'
import type { TerminalPaneSplitSource } from '../../../../shared/feature-education-telemetry'
import type { EventProps } from '../../../../shared/telemetry-events'
import { resolveTerminalFontWeights } from '../../../../shared/terminal-fonts'
import {
@@ -74,6 +74,18 @@ import {
type CloseTerminalPaneDetail
} from '@/constants/terminal'
import { acquireWebviewsDragPassthrough } from '../browser-pane/webview-registry'
import { recordCreatedTerminalPaneSplit } from './terminal-pane-split-completion'
export function recordRuntimeCreatedTerminalPaneSplit(
createdPane: unknown,
args: {
source: TerminalPaneSplitSource
direction: 'vertical' | 'horizontal'
telemetrySuppressed?: boolean
}
): boolean {
return recordCreatedTerminalPaneSplit(createdPane, args)
}
function extractUncHost(value: string | undefined): string | null {
const match = /^(?:\\\\|\/\/)([^\\/]+)/.exec(value ?? '')
@@ -1043,6 +1055,8 @@ export function useTerminalPaneLifecycle({
// than relying on getPanes()[0] which returns insertion order, not visual order.
const initialPane = manager.getActivePane() ?? manager.getPanes()[0]
// Why: setup/issue automation panes are internal workspace bootstrap flows,
// not the user-visible split-terminal milestone recorded below.
if (setupSplit) {
if (initialPane) {
const setupPane = splitPaneWithOneShotStartup(
@@ -1124,23 +1138,20 @@ export function useTerminalPaneLifecycle({
const createdPane = splitPaneWithOneShotStartup(ptyDeps, { command: detail.command }, () =>
mgr.splitPane(sourcePaneId, detail.direction, splitOptions)
)
if (createdPane) {
trackTerminalPaneSplit({
source: detail.telemetrySource ?? 'command',
direction: detail.direction
})
}
recordRuntimeCreatedTerminalPaneSplit(createdPane, {
source: detail.telemetrySource ?? 'command',
direction: detail.direction
})
} else {
const createdPane = mgr.splitPane(sourcePaneId, detail.direction, splitOptions)
if (
createdPane &&
!consumePendingWebRuntimeSplitMirrorTelemetry(detail.sourcePtyId, detail.direction)
) {
trackTerminalPaneSplit({
source: detail.telemetrySource ?? 'command',
direction: detail.direction
})
}
const telemetrySuppressed = createdPane
? consumePendingWebRuntimeSplitMirrorTelemetry(detail.sourcePtyId, detail.direction)
: false
recordRuntimeCreatedTerminalPaneSplit(createdPane, {
source: detail.telemetrySource ?? 'command',
direction: detail.direction,
telemetrySuppressed
})
}
}
window.addEventListener(SPLIT_TERMINAL_PANE_EVENT, onCliSplitPane)
+2 -4
View File
@@ -392,7 +392,7 @@ describe('TabsSlice', () => {
expect(sourceGroup?.activeTabId).toBe(t1.id)
})
it('records the split-pane command interaction when creating an empty split group', () => {
it('records generic pane interaction when creating an empty split group', () => {
const setMock = vi.mocked(window.api.ui.set)
store.getState().hydratePersistedUI(getDefaultUIState())
setMock.mockClear()
@@ -401,9 +401,7 @@ describe('TabsSlice', () => {
store.getState().createEmptySplitGroup(WT, sourceGroupId, 'right')
expect(store.getState().featureInteractions['terminal-pane-split']).toMatchObject({
interactionCount: 1
})
expect(store.getState().featureInteractions['terminal-pane-split']).toBeUndefined()
expect(store.getState().featureInteractions['terminal-panes']).toMatchObject({
interactionCount: 1
})
-1
View File
@@ -1092,7 +1092,6 @@ export const createTabsSlice: StateCreator<AppState, [], [], TabsSlice> = (set,
activeGroupIdByWorktree: { ...state.activeGroupIdByWorktree, [worktreeId]: newGroupId }
}
})
get().recordFeatureInteraction?.('terminal-pane-split')
get().recordFeatureInteraction?.('terminal-panes')
return newGroupId
},