perf(status-bar): avoid resource badge rebuilds on title churn (#8152)
This commit is contained in:
parent
141b9b7de2
commit
90afbc3cf3
|
|
@ -56,8 +56,10 @@ import {
|
|||
} from './resource-session-navigation'
|
||||
import {
|
||||
getResourceUsageAllWorktrees,
|
||||
getResourceUsagePtyIdsByTabId,
|
||||
getResourceUsageRepos,
|
||||
getResourceUsageRuntimePaneTitlesByTabId,
|
||||
getResourceUsageTerminalLayoutsByTabId,
|
||||
getResourceUsageTabsByWorktree
|
||||
} from './resource-usage-open-slices'
|
||||
import {
|
||||
|
|
@ -73,9 +75,11 @@ import {
|
|||
countUnboundDaemonSessions,
|
||||
type ResourceSessionBindingInputs
|
||||
} from './resource-session-bindings'
|
||||
import { createClosedResourceSessionCountSelector } from './resource-session-count-selector'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
const POLL_MS = 2_000
|
||||
const selectClosedResourceSessionCount = createClosedResourceSessionCountSelector()
|
||||
|
||||
type SortOption = 'memory' | 'cpu' | 'name'
|
||||
|
||||
|
|
@ -737,9 +741,7 @@ export function ResourceUsageStatusSegment({
|
|||
const memorySnapshotError = useAppStore((s) => s.memorySnapshotError)
|
||||
const fetchSnapshot = useAppStore((s) => s.fetchMemorySnapshot)
|
||||
const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady)
|
||||
const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId)
|
||||
const tabsByWorktreeForPtyBindings = useAppStore((s) => s.tabsByWorktree)
|
||||
const terminalLayoutsByTabId = useAppStore((s) => s.terminalLayoutsByTabId)
|
||||
const closedSessionCount = useAppStore(selectClosedResourceSessionCount)
|
||||
const setActiveView = useAppStore((s) => s.setActiveView)
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const openSpacePage = useAppStore((s) => s.openSpacePage)
|
||||
|
|
@ -774,17 +776,21 @@ export function ResourceUsageStatusSegment({
|
|||
const repos = useAppStore((s) => getResourceUsageRepos(s, open))
|
||||
const allWorktrees = useAppStore((s) => getResourceUsageAllWorktrees(s, open))
|
||||
const tabsByWorktree = useAppStore((s) => getResourceUsageTabsByWorktree(s, open))
|
||||
// Why: the closed trigger owns a scalar selector. Full binding maps stay
|
||||
// behind open sentinels so unchanged counts do not rerender the segment.
|
||||
const ptyIdsByTabId = useAppStore((s) => getResourceUsagePtyIdsByTabId(s, open))
|
||||
const terminalLayoutsByTabId = useAppStore((s) => getResourceUsageTerminalLayoutsByTabId(s, open))
|
||||
const resourceSnapshot = snapshot
|
||||
// Why: ptyIdsByTabId intentionally tracks mounted/live panes only. Resource
|
||||
// Manager also reads restored wake hints, but only for classification.
|
||||
const resourceSessionBindings = useMemo<ResourceSessionBindingInputs>(
|
||||
() => ({
|
||||
ptyIdsByTabId,
|
||||
tabsByWorktree: tabsByWorktreeForPtyBindings,
|
||||
tabsByWorktree,
|
||||
terminalLayoutsByTabId,
|
||||
workspaceSessionReady
|
||||
}),
|
||||
[ptyIdsByTabId, tabsByWorktreeForPtyBindings, terminalLayoutsByTabId, workspaceSessionReady]
|
||||
[ptyIdsByTabId, tabsByWorktree, terminalLayoutsByTabId, workspaceSessionReady]
|
||||
)
|
||||
|
||||
// Why: after a kill confirms and the session unmounts, focus would otherwise
|
||||
|
|
@ -975,12 +981,6 @@ export function ResourceUsageStatusSegment({
|
|||
return countUnboundDaemonSessions(sessions, resourceSessionBindings)
|
||||
}, [open, sessions, resourceSessionBindings, workspaceSessionReady])
|
||||
|
||||
const closedSessionCount = useMemo(() => {
|
||||
if (!workspaceSessionReady) {
|
||||
return 0
|
||||
}
|
||||
return buildResourceSessionBindingIndex(resourceSessionBindings).boundPtyIds.size
|
||||
}, [resourceSessionBindings, workspaceSessionReady])
|
||||
const triggerSessionCount = open ? sessions.length : closedSessionCount
|
||||
|
||||
const { totalMemory, totalCpu, hostShare, memBadgeLabel } = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,219 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types'
|
||||
import {
|
||||
buildResourceSessionBindingIndex,
|
||||
type ResourceSessionBindingInputs
|
||||
} from './resource-session-bindings'
|
||||
import {
|
||||
createClosedResourceSessionCountSelector,
|
||||
type ClosedResourceSessionCountState
|
||||
} from './resource-session-count-selector'
|
||||
|
||||
const TAB_COUNT = 100
|
||||
const TITLE_WRITES = 600
|
||||
|
||||
function makeTab(id: string, ptyId: string | null = null, title = 'Terminal'): TerminalTab {
|
||||
return {
|
||||
id,
|
||||
ptyId,
|
||||
worktreeId: `wt-${id}`,
|
||||
title,
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0
|
||||
}
|
||||
}
|
||||
|
||||
function makeLayout(ptyIdsByLeafId: Record<string, string>): TerminalLayoutSnapshot {
|
||||
return {
|
||||
root: { type: 'leaf', leafId: 'leaf-1' },
|
||||
activeLeafId: 'leaf-1',
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId
|
||||
}
|
||||
}
|
||||
|
||||
function makeState(
|
||||
overrides: Partial<ClosedResourceSessionCountState> = {}
|
||||
): ClosedResourceSessionCountState {
|
||||
return {
|
||||
tabsByWorktree: {},
|
||||
ptyIdsByTabId: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
workspaceSessionReady: true,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function withCountedIteration(tabs: TerminalTab[], onVisit: () => void): TerminalTab[] {
|
||||
return new Proxy(tabs, {
|
||||
get(target, property, receiver) {
|
||||
if (property !== Symbol.iterator) {
|
||||
return Reflect.get(target, property, receiver)
|
||||
}
|
||||
return function* countedIterator(): Generator<TerminalTab> {
|
||||
for (const tab of target) {
|
||||
onVisit()
|
||||
yield tab
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function makeScaleTabs(onVisit: () => void): Record<string, TerminalTab[]> {
|
||||
return Object.fromEntries(
|
||||
Array.from({ length: TAB_COUNT }, (_, index) => {
|
||||
const worktreeId = `wt-${index}`
|
||||
return [worktreeId, withCountedIteration([makeTab(`tab-${index}`, `pty-${index}`)], onVisit)]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function countNextTabComparison(
|
||||
tab: TerminalTab,
|
||||
onComparison: () => void
|
||||
): [TerminalTab[], () => void] {
|
||||
let counting = true
|
||||
const tabs = new Proxy([tab], {
|
||||
get(target, property, receiver) {
|
||||
if (counting && property === '0') {
|
||||
onComparison()
|
||||
}
|
||||
return Reflect.get(target, property, receiver)
|
||||
}
|
||||
})
|
||||
return [
|
||||
tabs,
|
||||
() => {
|
||||
counting = false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
describe('closed resource session count selector', () => {
|
||||
it('removes full binding-index rebuilds from title-only tab churn at scale', () => {
|
||||
let oldPathTabVisits = 0
|
||||
const initialTabs = makeScaleTabs(() => {
|
||||
oldPathTabVisits += 1
|
||||
})
|
||||
const inputs: ResourceSessionBindingInputs = {
|
||||
tabsByWorktree: initialTabs,
|
||||
ptyIdsByTabId: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
workspaceSessionReady: true
|
||||
}
|
||||
|
||||
// Why: this models the previous useMemo dependency on tabsByWorktree:
|
||||
// every title write replaces that map and rebuilds both tab passes.
|
||||
for (let index = 0; index < TITLE_WRITES; index += 1) {
|
||||
buildResourceSessionBindingIndex({ ...inputs, tabsByWorktree: { ...initialTabs } })
|
||||
}
|
||||
expect(oldPathTabVisits).toBe(120_000)
|
||||
|
||||
let optimizedTabVisits = 0
|
||||
const optimizedTabs = makeScaleTabs(() => {
|
||||
optimizedTabVisits += 1
|
||||
})
|
||||
const buildIndex = vi.fn(buildResourceSessionBindingIndex)
|
||||
const selectCount = createClosedResourceSessionCountSelector(buildIndex)
|
||||
let state = makeState({ tabsByWorktree: optimizedTabs })
|
||||
const currentTabByWorktree = Object.fromEntries(
|
||||
Object.entries(optimizedTabs).map(([worktreeId, tabs]) => [worktreeId, tabs[0]])
|
||||
)
|
||||
let changedArrayTabComparisons = 0
|
||||
|
||||
expect(selectCount(state)).toBe(TAB_COUNT)
|
||||
buildIndex.mockClear()
|
||||
optimizedTabVisits = 0
|
||||
|
||||
for (let index = 0; index < TITLE_WRITES; index += 1) {
|
||||
const worktreeId = `wt-${index % TAB_COUNT}`
|
||||
const nextTab = { ...currentTabByWorktree[worktreeId], title: `Terminal ${index}` }
|
||||
const [nextTabs, stopCounting] = countNextTabComparison(nextTab, () => {
|
||||
changedArrayTabComparisons += 1
|
||||
})
|
||||
state = {
|
||||
...state,
|
||||
tabsByWorktree: {
|
||||
...state.tabsByWorktree,
|
||||
[worktreeId]: nextTabs
|
||||
}
|
||||
}
|
||||
expect(selectCount(state)).toBe(TAB_COUNT)
|
||||
stopCounting()
|
||||
currentTabByWorktree[worktreeId] = nextTab
|
||||
}
|
||||
|
||||
expect(buildIndex).not.toHaveBeenCalled()
|
||||
expect(optimizedTabVisits).toBe(0)
|
||||
expect(changedArrayTabComparisons).toBe(TITLE_WRITES)
|
||||
})
|
||||
|
||||
it('reacts to every binding and readiness input while ignoring display-only tab fields', () => {
|
||||
const buildIndex = vi.fn(buildResourceSessionBindingIndex)
|
||||
const selectCount = createClosedResourceSessionCountSelector(buildIndex)
|
||||
let state = makeState({
|
||||
workspaceSessionReady: false,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [makeTab('tab-1', 'pty-wake')]
|
||||
},
|
||||
ptyIdsByTabId: {
|
||||
'tab-1': ['pty-live']
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-1': makeLayout({ 'leaf-1': 'pty-layout' })
|
||||
}
|
||||
})
|
||||
|
||||
expect(selectCount(state)).toBe(0)
|
||||
expect(buildIndex).not.toHaveBeenCalled()
|
||||
|
||||
state = { ...state, workspaceSessionReady: true }
|
||||
expect(selectCount(state)).toBe(3)
|
||||
expect(buildIndex).toHaveBeenCalledTimes(1)
|
||||
|
||||
state = {
|
||||
...state,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ ...state.tabsByWorktree['wt-1'][0], title: 'Working' }]
|
||||
}
|
||||
}
|
||||
expect(selectCount(state)).toBe(3)
|
||||
expect(buildIndex).toHaveBeenCalledTimes(1)
|
||||
|
||||
state = {
|
||||
...state,
|
||||
ptyIdsByTabId: { 'tab-1': ['pty-live', 'pty-live-2'] }
|
||||
}
|
||||
expect(selectCount(state)).toBe(4)
|
||||
expect(buildIndex).toHaveBeenCalledTimes(2)
|
||||
|
||||
state = {
|
||||
...state,
|
||||
terminalLayoutsByTabId: {
|
||||
'tab-1': makeLayout({ 'leaf-1': 'pty-layout', 'leaf-2': 'pty-layout-2' })
|
||||
}
|
||||
}
|
||||
expect(selectCount(state)).toBe(5)
|
||||
expect(buildIndex).toHaveBeenCalledTimes(3)
|
||||
|
||||
state = {
|
||||
...state,
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ ...state.tabsByWorktree['wt-1'][0], ptyId: null }]
|
||||
}
|
||||
}
|
||||
expect(selectCount(state)).toBe(4)
|
||||
expect(buildIndex).toHaveBeenCalledTimes(4)
|
||||
|
||||
state = { ...state, tabsByWorktree: {} }
|
||||
expect(selectCount(state)).toBe(2)
|
||||
expect(buildIndex).toHaveBeenCalledTimes(5)
|
||||
|
||||
state = { ...state, workspaceSessionReady: false }
|
||||
expect(selectCount(state)).toBe(0)
|
||||
expect(buildIndex).toHaveBeenCalledTimes(5)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import type { AppState } from '../../store'
|
||||
import {
|
||||
buildResourceSessionBindingIndex,
|
||||
type ResourceSessionBindingIndex,
|
||||
type ResourceSessionBindingInputs
|
||||
} from './resource-session-bindings'
|
||||
|
||||
export type ClosedResourceSessionCountState = Pick<
|
||||
AppState,
|
||||
'tabsByWorktree' | 'ptyIdsByTabId' | 'terminalLayoutsByTabId' | 'workspaceSessionReady'
|
||||
>
|
||||
|
||||
type BuildResourceSessionBindingIndex = (
|
||||
inputs: ResourceSessionBindingInputs
|
||||
) => ResourceSessionBindingIndex
|
||||
|
||||
export type ClosedResourceSessionCountSelector = (state: ClosedResourceSessionCountState) => number
|
||||
|
||||
function haveSameTabBindings(
|
||||
previous: AppState['tabsByWorktree'],
|
||||
next: AppState['tabsByWorktree']
|
||||
): boolean {
|
||||
if (previous === next) {
|
||||
return true
|
||||
}
|
||||
|
||||
const previousWorktreeIds = Object.keys(previous)
|
||||
const nextWorktreeIds = Object.keys(next)
|
||||
if (previousWorktreeIds.length !== nextWorktreeIds.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const worktreeId of nextWorktreeIds) {
|
||||
const previousTabs = previous[worktreeId]
|
||||
const nextTabs = next[worktreeId]
|
||||
if (previousTabs === nextTabs) {
|
||||
continue
|
||||
}
|
||||
if (!previousTabs || previousTabs.length !== nextTabs.length) {
|
||||
return false
|
||||
}
|
||||
for (let index = 0; index < nextTabs.length; index += 1) {
|
||||
const previousTab = previousTabs[index]
|
||||
const nextTab = nextTabs[index]
|
||||
// Why: the closed badge counts PTY ownership only. Titles and other
|
||||
// display fields can churn per terminal frame without changing it.
|
||||
if (previousTab.id !== nextTab.id || previousTab.ptyId !== nextTab.ptyId) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function createClosedResourceSessionCountSelector(
|
||||
buildBindingIndex: BuildResourceSessionBindingIndex = buildResourceSessionBindingIndex
|
||||
): ClosedResourceSessionCountSelector {
|
||||
// Why: Zustand runs selectors for every store notification. Keep the last
|
||||
// liveness inputs here so unrelated and title-only writes stay scalar-cheap.
|
||||
let initialized = false
|
||||
let previousTabsByWorktree: AppState['tabsByWorktree'] = {}
|
||||
let previousPtyIdsByTabId: AppState['ptyIdsByTabId'] = {}
|
||||
let previousTerminalLayoutsByTabId: AppState['terminalLayoutsByTabId'] = {}
|
||||
let previousWorkspaceSessionReady = false
|
||||
let count = 0
|
||||
|
||||
return (state): number => {
|
||||
const bindingMapChanged =
|
||||
state.ptyIdsByTabId !== previousPtyIdsByTabId ||
|
||||
state.terminalLayoutsByTabId !== previousTerminalLayoutsByTabId
|
||||
const readinessChanged = state.workspaceSessionReady !== previousWorkspaceSessionReady
|
||||
const tabsReferenceChanged = state.tabsByWorktree !== previousTabsByWorktree
|
||||
let tabBindingsChanged = tabsReferenceChanged
|
||||
if (
|
||||
initialized &&
|
||||
state.workspaceSessionReady &&
|
||||
!bindingMapChanged &&
|
||||
!readinessChanged &&
|
||||
tabsReferenceChanged
|
||||
) {
|
||||
tabBindingsChanged = !haveSameTabBindings(previousTabsByWorktree, state.tabsByWorktree)
|
||||
}
|
||||
|
||||
const shouldRebuild =
|
||||
state.workspaceSessionReady &&
|
||||
(!initialized || bindingMapChanged || readinessChanged || tabBindingsChanged)
|
||||
|
||||
if (shouldRebuild) {
|
||||
count = buildBindingIndex(state).boundPtyIds.size
|
||||
} else if (!state.workspaceSessionReady) {
|
||||
count = 0
|
||||
}
|
||||
|
||||
previousTabsByWorktree = state.tabsByWorktree
|
||||
previousPtyIdsByTabId = state.ptyIdsByTabId
|
||||
previousTerminalLayoutsByTabId = state.terminalLayoutsByTabId
|
||||
previousWorkspaceSessionReady = state.workspaceSessionReady
|
||||
initialized = true
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getResourceUsageAllWorktrees,
|
||||
getResourceUsagePtyIdsByTabId,
|
||||
getResourceUsageRepos,
|
||||
getResourceUsageRuntimePaneTitlesByTabId,
|
||||
getResourceUsageTerminalLayoutsByTabId,
|
||||
getResourceUsageTabsByWorktree
|
||||
} from './resource-usage-open-slices'
|
||||
import type { AppState } from '../../store'
|
||||
|
|
@ -41,11 +43,21 @@ const worktree = (): AppState['worktreesByRepo'][string][number] => ({
|
|||
describe('resource usage open slices', () => {
|
||||
it('returns stable empty slices while the popover is closed', () => {
|
||||
const tabsByWorktree = { 'wt-1': [terminalTab('tab-1')] }
|
||||
const ptyIdsByTabId = { 'tab-1': ['pty-1'] }
|
||||
const terminalLayoutsByTabId = {
|
||||
'tab-1': {
|
||||
root: { type: 'leaf' as const, leafId: 'leaf-1' },
|
||||
activeLeafId: 'leaf-1',
|
||||
expandedLeafId: null
|
||||
}
|
||||
}
|
||||
const runtimePaneTitlesByTabId = {
|
||||
'tab-1': { 'tab-1:0': 'Working' }
|
||||
} as AppState['runtimePaneTitlesByTabId']
|
||||
|
||||
const closedTabs = getResourceUsageTabsByWorktree({ tabsByWorktree }, false)
|
||||
const closedPtyIds = getResourceUsagePtyIdsByTabId({ ptyIdsByTabId }, false)
|
||||
const closedLayouts = getResourceUsageTerminalLayoutsByTabId({ terminalLayoutsByTabId }, false)
|
||||
const closedTitles = getResourceUsageRuntimePaneTitlesByTabId(
|
||||
{ runtimePaneTitlesByTabId },
|
||||
false
|
||||
|
|
@ -55,17 +67,35 @@ describe('resource usage open slices', () => {
|
|||
expect(closedTitles).toBe(
|
||||
getResourceUsageRuntimePaneTitlesByTabId({ runtimePaneTitlesByTabId: {} }, false)
|
||||
)
|
||||
expect(closedPtyIds).toBe(getResourceUsagePtyIdsByTabId({ ptyIdsByTabId: {} }, false))
|
||||
expect(closedLayouts).toBe(
|
||||
getResourceUsageTerminalLayoutsByTabId({ terminalLayoutsByTabId: {} }, false)
|
||||
)
|
||||
expect(closedTabs).toEqual({})
|
||||
expect(closedPtyIds).toEqual({})
|
||||
expect(closedLayouts).toEqual({})
|
||||
expect(closedTitles).toEqual({})
|
||||
})
|
||||
|
||||
it('returns live slices while the popover is open', () => {
|
||||
const tabsByWorktree = { 'wt-1': [terminalTab('tab-1')] }
|
||||
const ptyIdsByTabId = { 'tab-1': ['pty-1'] }
|
||||
const terminalLayoutsByTabId = {
|
||||
'tab-1': {
|
||||
root: { type: 'leaf' as const, leafId: 'leaf-1' },
|
||||
activeLeafId: 'leaf-1',
|
||||
expandedLeafId: null
|
||||
}
|
||||
}
|
||||
const runtimePaneTitlesByTabId = {
|
||||
'tab-1': { 'tab-1:0': 'Working' }
|
||||
} as AppState['runtimePaneTitlesByTabId']
|
||||
|
||||
expect(getResourceUsageTabsByWorktree({ tabsByWorktree }, true)).toBe(tabsByWorktree)
|
||||
expect(getResourceUsagePtyIdsByTabId({ ptyIdsByTabId }, true)).toBe(ptyIdsByTabId)
|
||||
expect(getResourceUsageTerminalLayoutsByTabId({ terminalLayoutsByTabId }, true)).toBe(
|
||||
terminalLayoutsByTabId
|
||||
)
|
||||
expect(getResourceUsageRuntimePaneTitlesByTabId({ runtimePaneTitlesByTabId }, true)).toBe(
|
||||
runtimePaneTitlesByTabId
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import type { AppState } from '../../store'
|
|||
import { getAllWorktreesFromState } from '../../store/selectors'
|
||||
|
||||
const EMPTY_TABS_BY_WORKTREE: AppState['tabsByWorktree'] = {}
|
||||
const EMPTY_PTY_IDS_BY_TAB_ID: AppState['ptyIdsByTabId'] = {}
|
||||
const EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID: AppState['terminalLayoutsByTabId'] = {}
|
||||
const EMPTY_RUNTIME_PANE_TITLES_BY_TAB_ID: AppState['runtimePaneTitlesByTabId'] = {}
|
||||
const EMPTY_REPOS: AppState['repos'] = []
|
||||
const EMPTY_WORKTREES: ReturnType<typeof getAllWorktreesFromState> = []
|
||||
|
|
@ -13,6 +15,20 @@ export function getResourceUsageTabsByWorktree(
|
|||
return open ? state.tabsByWorktree : EMPTY_TABS_BY_WORKTREE
|
||||
}
|
||||
|
||||
export function getResourceUsagePtyIdsByTabId(
|
||||
state: Pick<AppState, 'ptyIdsByTabId'>,
|
||||
open: boolean
|
||||
): AppState['ptyIdsByTabId'] {
|
||||
return open ? state.ptyIdsByTabId : EMPTY_PTY_IDS_BY_TAB_ID
|
||||
}
|
||||
|
||||
export function getResourceUsageTerminalLayoutsByTabId(
|
||||
state: Pick<AppState, 'terminalLayoutsByTabId'>,
|
||||
open: boolean
|
||||
): AppState['terminalLayoutsByTabId'] {
|
||||
return open ? state.terminalLayoutsByTabId : EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID
|
||||
}
|
||||
|
||||
export function getResourceUsageRuntimePaneTitlesByTabId(
|
||||
state: Pick<AppState, 'runtimePaneTitlesByTabId'>,
|
||||
open: boolean
|
||||
|
|
|
|||
Loading…
Reference in New Issue