refactor(renderer): organize status bar owners (#42456)

This commit is contained in:
Neil
2026-11-14 11:03:20 -08:00
committed by GitHub
parent 65c3de827e
commit 9c7be3d42a
205 changed files with 640 additions and 1843 deletions
+11 -11
View File
@@ -137,18 +137,18 @@
1 src/renderer/src/components/skills/SkillsPage.tsx
1 src/renderer/src/components/skills/setup/linear-agent-skill-install-cta.tsx
1 src/renderer/src/components/source-control/BaseRefPicker.tsx
1 src/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsx
1 src/renderer/src/components/status-bar/CodexSwitcherMenu.tsx
1 src/renderer/src/components/status-bar/SshStatusSegment.tsx
1 src/renderer/src/components/status-bar/accounts/ClaudeSwitcherMenu.tsx
1 src/renderer/src/components/status-bar/accounts/CodexSwitcherMenu.tsx
1 src/renderer/src/components/status-bar/host-connections/SshStatusSegment.tsx
1 src/renderer/src/components/status-bar/StatusBarSurface.tsx
2 src/renderer/src/components/status-bar/provider-account-sync-key.ts
1 src/renderer/src/components/status-bar/usage-provider-settings-target.ts
1 src/renderer/src/components/status-bar/use-claude-switcher-state.ts
2 src/renderer/src/components/status-bar/use-codex-switcher-controller.ts
1 src/renderer/src/components/status-bar/use-status-bar-controller.ts
1 src/renderer/src/components/status-bar/use-workspace-space-git-refresh-action.ts
1 src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts
1 src/renderer/src/components/status-bar/workspace-space-decision-details.ts
2 src/renderer/src/components/status-bar/accounts/provider-account-sync-key.ts
1 src/renderer/src/components/status-bar/usage/usage-provider-settings-target.ts
1 src/renderer/src/components/status-bar/accounts/use-claude-switcher-state.ts
2 src/renderer/src/components/status-bar/accounts/use-codex-switcher-controller.ts
1 src/renderer/src/components/status-bar/composition/use-status-bar-controller.ts
1 src/renderer/src/components/status-bar/workspace-storage/decisions/use-workspace-space-git-refresh-action.ts
1 src/renderer/src/components/status-bar/workspace-storage/manager/use-workspace-space-manager-panel.ts
1 src/renderer/src/components/status-bar/workspace-storage/decisions/workspace-space-decision-details.ts
1 src/renderer/src/components/tab-bar/TabBar.tsx
2 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts
1 src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts
+2 -2
View File
@@ -9,7 +9,7 @@
1 src/renderer/src/components/settings/quick-commands/QuickCommandsToolbar.tsx
1 src/renderer/src/components/settings/repository/hosts/RepositoryHostSetupsSection.tsx
1 src/renderer/src/components/sidebar/host-admission/ssh/AddRemoteHostSshConfigPicker.tsx
4 src/renderer/src/components/status-bar/SshStatusSegment.tsx
1 src/renderer/src/components/status-bar/resource-usage-popover-body.tsx
4 src/renderer/src/components/status-bar/host-connections/SshStatusSegment.tsx
1 src/renderer/src/components/status-bar/resources/popover/resource-usage-popover-body.tsx
1 src/renderer/src/components/tab-bar/TabBarQuickCommandAddActions.tsx
2 src/renderer/src/components/terminal-pane/quick-commands/TerminalQuickCommandsSubmenu.tsx
+12 -12
View File
@@ -550,18 +550,18 @@
1 src/renderer/src/components/ssh-host-recovery/SshHostMoveAnywayAction.tsx
2 src/renderer/src/components/star-nag/StarNagAgentValueMomentObserver.tsx
1 src/renderer/src/components/stats/ShareUsageButton.tsx
2 src/renderer/src/components/status-bar/ClaudeSwitcherForOwner.tsx
1 src/renderer/src/components/status-bar/PortsStatusSegment.tsx
1 src/renderer/src/components/status-bar/RuntimeHostStatusRow.tsx
2 src/renderer/src/components/status-bar/SshTargetStatusRow.tsx
1 src/renderer/src/components/status-bar/icons.tsx
3 src/renderer/src/components/status-bar/status-bar-density.ts
2 src/renderer/src/components/status-bar/use-codex-switcher-controller.ts
1 src/renderer/src/components/status-bar/use-owner-usage-rate-limits.ts
1 src/renderer/src/components/status-bar/use-provider-account-list.ts
1 src/renderer/src/components/status-bar/use-resource-usage-actions.ts
1 src/renderer/src/components/status-bar/use-resource-view-visible.ts
1 src/renderer/src/components/status-bar/use-status-bar-controller.ts
2 src/renderer/src/components/status-bar/accounts/ClaudeSwitcherForOwner.tsx
1 src/renderer/src/components/status-bar/host-connections/PortsStatusSegment.tsx
1 src/renderer/src/components/status-bar/host-connections/RuntimeHostStatusRow.tsx
2 src/renderer/src/components/status-bar/host-connections/SshTargetStatusRow.tsx
1 src/renderer/src/components/status-bar/accounts/provider-icons.tsx
3 src/renderer/src/components/status-bar/composition/status-bar-density.ts
2 src/renderer/src/components/status-bar/accounts/use-codex-switcher-controller.ts
1 src/renderer/src/components/status-bar/usage/use-owner-usage-rate-limits.ts
1 src/renderer/src/components/status-bar/accounts/use-provider-account-list.ts
1 src/renderer/src/components/status-bar/resources/session-actions/use-resource-usage-actions.ts
1 src/renderer/src/components/status-bar/resources/use-resource-view-visible.ts
1 src/renderer/src/components/status-bar/composition/use-status-bar-controller.ts
2 src/renderer/src/components/tab-bar/TabBar.tsx
2 src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx
1 src/renderer/src/components/tab-bar/TabStripScrollIndicator.tsx
+2 -2
View File
@@ -76,8 +76,8 @@
1 src/renderer/src/components/sidebar/SeveredTerminalDaemonBanner.tsx
1 src/renderer/src/components/sidebar/StalledTerminalDaemonBanner.tsx
1 src/renderer/src/components/sidebar/workspace-card/activity/prompt-cache-countdown-clock.ts
1 src/renderer/src/components/status-bar/use-resource-memory-snapshot.ts
1 src/renderer/src/components/status-bar/use-runtime-host-resources.ts
1 src/renderer/src/components/status-bar/resources/use-resource-memory-snapshot.ts
1 src/renderer/src/components/status-bar/host-connections/use-runtime-host-resources.ts
1 src/renderer/src/components/tab-bar/tab-strip-drag-scroll.ts
1 src/renderer/src/components/terminal-pane/pty-transport/delivery/terminal-delivery-watchdog.ts
1 src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/RemoteRuntimeRecoveryCard.tsx
+1 -1
View File
@@ -10,7 +10,7 @@
1 src/renderer/src/components/sidebar/workspace-card/activity/use-worktree-activity-statuses.ts
4 src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-send-target-inputs.ts
4 src/renderer/src/components/sidebar/worktree-list/listing/use-agent-send-target.ts
1 src/renderer/src/components/status-bar/workspace-space-decision-inputs.ts
1 src/renderer/src/components/status-bar/workspace-storage/decisions/workspace-space-decision-inputs.ts
2 src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts
3 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model-worktree-write-probe.ts
1 src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
@@ -1,6 +1,6 @@
import type React from 'react'
import { getAgentCatalog, getAgentLabel } from '../../../lib/agent-catalog/agent-catalog'
import { ClaudeIcon, DroidIcon, OpenAIIcon } from '@/components/status-bar/icons'
import { ClaudeIcon, DroidIcon, OpenAIIcon } from '@/components/status-bar/accounts/provider-icons'
import type { TerminalAgent } from '../../../../../shared/agent-status/identity/terminal-agent'
import {
AgentLetterIcon,
@@ -1,7 +1,7 @@
import { useTranslate } from '@/i18n/use-translate'
import { isClipboardTextByteLengthOverLimit } from '../../../../../../shared/clipboard-text'
import { LARGE_MARKDOWN_PREVIEW_MAX_BYTES } from '../document-worker/markdown-preview-document-types'
import '../../../status-bar/workspace-space-format'
import '../../../status-bar/workspace-storage/workspace-space-format'
import { formatBytes } from '../../../../../../shared/format-bytes'
export function MarkdownPreviewSizeGate({
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import type { JSX } from 'react'
import { ChevronDown, Workflow } from 'lucide-react'
import { ClaudeIcon, OpenAIIcon } from '../../status-bar/icons'
import { ClaudeIcon, OpenAIIcon } from '../../status-bar/accounts/provider-icons'
import {
BUBBLE_FLIGHT_MS,
BUBBLE_GAP_MS,
@@ -4,7 +4,7 @@ import { Wrench } from 'lucide-react'
import { AgentStateDot } from '@/components/AgentStateDot'
import { getAgentCatalog, type AgentCatalogEntry } from '@/lib/agent-catalog/agent-catalog'
import { AgentIcon } from '@/components/agent/icons/AgentIcon'
import { ClaudeIcon, OpenAIIcon } from '../../status-bar/icons'
import { ClaudeIcon, OpenAIIcon } from '../../status-bar/accounts/provider-icons'
import { installWindowVisibilityInterval } from '@/lib/window-activity/window-visibility-interval'
import { cn } from '@/lib/class-names'
import { useTranslate } from '@/i18n/use-translate'
@@ -4,7 +4,7 @@ import { Loader2, Plus } from 'lucide-react'
import { toast } from 'sonner'
import { useAppStore } from '@/store/index'
import { Button } from '@/components/ui/button'
import { ClaudeIcon, OpenAIIcon } from '@/components/status-bar/icons'
import { ClaudeIcon, OpenAIIcon } from '@/components/status-bar/accounts/provider-icons'
import { cn } from '@/lib/class-names'
import { readIpcErrorMessage } from '@/runtime/errors/ipc-error'
import { useMountedRef } from '@/hooks/useMountedRef'
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import type { JSX } from 'react'
import { ClaudeIcon, OpenAIIcon } from '../../status-bar/icons'
import { ClaudeIcon, OpenAIIcon } from '../../status-bar/accounts/provider-icons'
import { cn } from '@/lib/class-names'
import { useTranslate } from '@/i18n/use-translate'
@@ -1,5 +1,5 @@
import type { JSX, ReactNode } from 'react'
import { ClaudeIcon } from '@/components/status-bar/icons'
import { ClaudeIcon } from '@/components/status-bar/accounts/provider-icons'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { cn } from '@/lib/class-names'
@@ -1,5 +1,5 @@
import type { JSX, ReactNode } from 'react'
import { ClaudeIcon } from '@/components/status-bar/icons'
import { ClaudeIcon } from '@/components/status-bar/accounts/provider-icons'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/class-names'
import { FeatureWallClickRing } from '../storyboard-interaction/FeatureWallClickRing'
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import type { JSX } from 'react'
import { ArrowRight, CircleDot } from 'lucide-react'
import { AgentStateDot } from '@/components/AgentStateDot'
import { ClaudeIcon } from '../../status-bar/icons'
import { ClaudeIcon } from '../../status-bar/accounts/provider-icons'
import { FeatureWallClickRing } from '../storyboard-interaction/FeatureWallClickRing'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
@@ -1,7 +1,7 @@
import type { JSX, ReactNode } from 'react'
import { useTranslate } from '@/i18n/use-translate'
import { cn } from '@/lib/class-names'
import { ClaudeIcon, OpenAIIcon } from '../../status-bar/icons'
import { ClaudeIcon, OpenAIIcon } from '../../status-bar/accounts/provider-icons'
import type {
WORKBENCH_RUN_QUEUE,
WorkbenchTerminalLine
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import type { JSX } from 'react'
import { AgentStateDot } from '@/components/AgentStateDot'
import { installWindowVisibilityInterval } from '@/lib/window-activity/window-visibility-interval'
import { ClaudeIcon, OpenCodeGoIcon } from '../../status-bar/icons'
import { ClaudeIcon, OpenCodeGoIcon } from '../../status-bar/accounts/provider-icons'
type AgentKind = 'claude' | 'codex' | 'opencode'
@@ -7,7 +7,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import type { FloatingTerminalTriggerLocation } from '../../../../shared/ui-chrome-types'
import { useTranslate } from '@/i18n/use-translate'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { connectRuntimeHostAndReloadProjects } from '@/components/status-bar/runtime-environment-explicit-connect'
import { connectRuntimeHostAndReloadProjects } from '@/components/status-bar/host-connections/runtime-environment-explicit-connect'
import { useMountedRef } from '@/hooks/useMountedRef'
import { useTranslate } from '@/i18n/use-translate'
import { startWslHostAndReloadProjects } from '@/runtime/hosts/wsl/wsl-host-user-start'
@@ -1,190 +0,0 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type { RuntimeEnvironmentStatus } from '../../../../../../shared/remote-runtime/host-status/runtime-host-status'
import type { AgentJournalRenderItem } from '../../../../../../shared/agent-session/journal/agent-session-journal-types'
const { mocks, moduleFactories, resetStructuredSessionMocks } = await vi.hoisted(async () =>
(await import('./NativeChatStructuredSession.test-harness')).createStructuredSessionMocks()
)
const reconnectHost = vi.hoisted(() => vi.fn<(environmentId: string) => Promise<boolean>>())
vi.mock('../../../../runtime/native-chat/requests/structured-agent-session-call', () =>
moduleFactories.structuredAgentSessionCall()
)
vi.mock('../../../../runtime/native-chat/requests/structured-agent-session-subscriptions', () =>
moduleFactories.structuredAgentSessionSubscriptions()
)
vi.mock('../../session/structured/use-structured-agent-session', () =>
moduleFactories.useStructuredAgentSession()
)
vi.mock('../../appearance/use-native-chat-font-size', () => moduleFactories.useNativeChatFontSize())
vi.mock('../../links/actions/use-native-chat-file-link-context', () =>
moduleFactories.useNativeChatFileLinkContext()
)
vi.mock('../../links/actions/use-native-chat-file-link-click', () =>
moduleFactories.useNativeChatFileLinkClick()
)
vi.mock('../../NativeChatMessageList', () => moduleFactories.nativeChatMessageList())
vi.mock('../../composer/NativeChatComposer', () => moduleFactories.nativeChatComposer())
vi.mock('../../prompts/NativeChatApprovalCard', () => moduleFactories.nativeChatApprovalCard())
vi.mock('../../prompts/NativeChatQuestionCard', () => moduleFactories.nativeChatQuestionCard())
vi.mock('@/components/status-bar/runtime-environment-explicit-connect', () => ({
connectRuntimeHostAndReloadProjects: reconnectHost
}))
import { useAppStore } from '@/store'
import { NativeChatStructuredSession } from '../../NativeChatStructuredSession'
const initialState = useAppStore.getState()
let sequence = 0
function hostStatus(
verification: 'verified' | 'unavailable' | 'blocked',
transport: 'ready' | 'disconnected',
retired?: true
): RuntimeEnvironmentStatus {
const status =
verification === 'verified'
? {
runtimeId: 'runtime-1',
rendererGraphEpoch: 1,
graphStatus: 'ready' as const,
authoritativeWindowId: 1,
desktopWindowStatus: 'available' as const,
liveTabCount: 0,
liveLeafCount: 0
}
: null
return {
snapshot: {
environmentId: 'remote-host',
pairingRevision: 1,
sequence: ++sequence,
checkedAt: 1,
status,
verification,
transport,
...(retired ? { retired } : {})
},
status,
checkedAt: 1
}
}
function setHost(status: RuntimeEnvironmentStatus): void {
act(() => {
useAppStore.setState({ runtimeStatusByEnvironmentId: new Map([['remote-host', status]]) })
})
}
function paneElement(): React.JSX.Element {
return (
<NativeChatStructuredSession
isVisible
isFocusedGroup
tabId="structured-host-outage-tab"
sessionId="host-outage-session"
target={{ kind: 'environment', environmentId: 'remote-host' }}
agent="claude"
/>
)
}
function renderPane() {
return render(paneElement())
}
function promptItem(kind: 'approval' | 'question'): AgentJournalRenderItem {
const options = [{ id: 'allow', label: 'Approve' }]
const resolution = {
state: 'pending' as const,
selectedOptionId: null,
resolvedBy: null,
resolvedAt: null
}
return {
itemId: 'host-outage-prompt',
revision: 1,
sequence: 1,
observedAt: 1,
body:
kind === 'approval'
? {
kind,
title: 'Run the plan?',
detail: null,
subject: { kind: 'plan', text: 'x' },
options,
resolution
}
: { kind, question: 'Run the plan?', options, resolution }
}
}
beforeEach(() => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
useAppStore.setState({
runtimeEnvironments: [
{
id: 'remote-host',
name: 'Build box',
createdAt: 1,
updatedAt: 1,
lastUsedAt: null,
runtimeId: null,
endpoints: [{ id: 'ws', kind: 'websocket', label: 'ws', endpoint: 'ws://host' }],
preferredEndpointId: 'ws'
}
],
runtimeStatusByEnvironmentId: new Map([['remote-host', hostStatus('verified', 'ready')]])
})
})
afterEach(() => {
cleanup()
vi.useRealTimers()
resetStructuredSessionMocks()
reconnectHost.mockReset()
useAppStore.setState({
runtimeEnvironments: initialState.runtimeEnvironments,
runtimeStatusByEnvironmentId: initialState.runtimeStatusByEnvironmentId
})
})
it.each(['approval', 'question'] as const)(
'keeps the host row and pending Reconnect when a %s takes the composer slot',
async (kind) => {
let finish: () => void = () => {}
reconnectHost.mockImplementation(
() =>
new Promise<boolean>((resolve) => {
finish = () => resolve(false)
})
)
const pane = renderPane()
setHost(hostStatus('blocked', 'disconnected', true))
fireEvent.click(screen.getByRole('button', { name: 'Reconnect' }))
mocks.promptItems = [promptItem(kind)]
pane.rerender(paneElement())
expect(screen.queryByTestId('structured-composer')).toBeNull()
expect(document.querySelector(`[data-native-chat-${kind}-card-mock]`)).toBeTruthy()
expect(screen.getAllByText('Build box is offline')).toHaveLength(1)
expect(screen.getByRole('listitem')).toHaveAttribute('data-notice-kind', 'host')
expect(screen.getByRole('button', { name: 'Reconnect' })).toBeDisabled()
expect(screen.queryByRole('button', { name: 'Dismiss' })).toBeNull()
const region = screen.getByRole('list')
await act(async () => {
finish()
})
expect(screen.getByRole('button', { name: 'Reconnect' })).toBeEnabled()
setHost(hostStatus('verified', 'ready'))
expect(screen.queryByText('Build box is offline')).toBeNull()
expect(screen.getByRole('list')).toBe(region)
expect(region).toHaveAttribute('aria-live', 'polite')
}
)
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { translate } from '@/i18n/i18n'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import type {
AgentSessionAccountUsage,
@@ -15,9 +15,12 @@ import type { CodexRateLimitResetOutcome } from '../../../../../shared/rate-limi
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
import { normalizeUsagePercentageDisplay } from '../../../../../shared/usage-percentage-display'
import { getProviderDisplayName } from '../../../../../shared/usage-provider-windows'
import { ProviderIdentityCard } from '../../status-bar/ProviderIdentityCard'
import { CodexResetConfirmation, CodexResetCredits } from '../../status-bar/CodexResetCredits'
import { formatResetCreditExpiry, ProviderIcon } from '../../status-bar/tooltip'
import { ProviderIdentityCard } from '../../status-bar/accounts/ProviderIdentityCard'
import {
CodexResetConfirmation,
CodexResetCredits
} from '../../status-bar/accounts/CodexResetCredits'
import { formatResetCreditExpiry, ProviderIcon } from '../../status-bar/usage/provider-usage-panel'
type UsageResult = Extract<AgentSessionAccountUsage, { status: 'ok' }>
@@ -17,8 +17,8 @@ import { useStructuredChatTabConversationName } from '@/runtime/native-chat/nami
import { AgentSessionContinuationDialog } from '@/components/agent-session-continuation/AgentSessionContinuationDialog'
import { buildAgentSessionContinuationPrompt } from '@/lib/agent-session/context/agent-session-continuation'
import type { AgentSessionContinuationRequest } from '@/lib/agent-session/context/agent-session-continuation'
import { ClaudeSwitcherMenu } from '@/components/status-bar/ClaudeSwitcherMenu'
import { ProviderAccountChatButton } from '@/components/status-bar/ProviderAccountChatButton'
import { ClaudeSwitcherMenu } from '@/components/status-bar/accounts/ClaudeSwitcherMenu'
import { ProviderAccountChatButton } from '@/components/status-bar/accounts/ProviderAccountChatButton'
import { isTuiAgent } from '../../../../../shared/agent-launch/catalog/tui-agent-config'
import { callStructuredAgentSession } from '@/runtime/native-chat/requests/structured-agent-session-call'
import { executionHostIdForStructuredTarget } from '@/runtime/native-chat/session-ownership/structured-agent-session-owner'
@@ -6,7 +6,7 @@ import { Badge } from '../../../ui/badge'
import { Button } from '../../../ui/button'
import { ButtonGroup } from '../../../ui/button-group'
import { Label } from '../../../ui/label'
import { ClaudeIcon } from '../../../status-bar/icons'
import { ClaudeIcon } from '../../../status-bar/accounts/provider-icons'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { ClaudeOAuthUsageSetting } from './claude-oauth-usage-setting'
import { ClaudeRunningSessionSwitchSetting } from './claude-running-session-switch-setting'
@@ -5,7 +5,7 @@ import { selectCodexProviderAccount } from '@/runtime/provider-accounts/observat
import { Badge } from '../../../ui/badge'
import { Button } from '../../../ui/button'
import { Label } from '../../../ui/label'
import { OpenAIIcon } from '../../../status-bar/icons'
import { OpenAIIcon } from '../../../status-bar/accounts/provider-icons'
import { CodexLoginLinkNotice } from './CodexLoginLinkNotice'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { ACCOUNTS_SECTION_IDS, getAccountsCodexSearchEntries } from '../accounts-search'
@@ -5,7 +5,7 @@ import { useTranslate, type Translate } from '@/i18n/use-translate'
import type { AntigravityAccountState } from '../../../../../../shared/antigravity-account-types'
import type { ProviderRateLimits } from '../../../../../../shared/rate-limit-types'
import { openAppHttpLinkFromAnchor } from '@/lib/browser/navigation/app-http-link-open'
import { formatTimeAgo } from '../../../status-bar/tooltip'
import { formatTimeAgo } from '../../../status-bar/usage/provider-usage-panel'
import { Badge } from '../../../ui/badge'
import { Button } from '../../../ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../ui/tooltip'
@@ -2,7 +2,7 @@ import { translate } from '../../../../i18n/i18n'
import type { Translate } from '../../../../i18n/use-translate'
import { Label } from '../../../ui/label'
import { Switch } from '../../../ui/switch'
import { GeminiIcon } from '../../../status-bar/icons'
import { GeminiIcon } from '../../../status-bar/accounts/provider-icons'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
import type { AccountsPaneSectionModel } from '../accounts-pane-types'
@@ -2,7 +2,7 @@ import { translate } from '../../../../i18n/i18n'
import type { Translate } from '../../../../i18n/use-translate'
import { Button } from '../../../ui/button'
import { Label } from '../../../ui/label'
import { OpenCodeGoIcon } from '../../../status-bar/icons'
import { OpenCodeGoIcon } from '../../../status-bar/accounts/provider-icons'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { SettingsGroup } from '../../section-layout/SettingsGroup'
import { DebouncedSettingsTextInput } from '../../form-controls/DebouncedSettingsTextInput'
@@ -4,7 +4,7 @@ import { cn } from '@/lib/class-names'
import type { FeatureInteractionId } from '../../../../../../shared/feature-interaction-catalog'
import type { ProviderRateLimits } from '../../../../../../shared/rate-limit-types'
import { Label } from '../../../ui/label'
import { MiniMaxIcon } from '../../../status-bar/icons'
import { MiniMaxIcon } from '../../../status-bar/accounts/provider-icons'
import { SearchableSetting } from '../../section-layout/SearchableSetting'
import { DebouncedSettingsTextInput } from '../../form-controls/DebouncedSettingsTextInput'
import { MiniMaxCredentials, miniMaxStorageHelp } from './accounts-pane-minimax-credentials'
@@ -12,7 +12,7 @@ import {
SettingsSegmentedControl,
SettingsSwitchRow
} from '../form-controls/SettingsFormControls'
import { useAvailableStatusBarToggles } from '../../status-bar/use-available-status-bar-toggles'
import { useAvailableStatusBarToggles } from '../../status-bar/composition/use-available-status-bar-toggles'
import { getLayoutEntries, getFollowSymlinkedDirectoriesEntry } from './appearance-search'
import {
getSidebarEntries,
@@ -4,13 +4,13 @@ import type { ProviderRateLimits } from '../../../../../shared/rate-limit-types'
import type { StatusBarUsageFormat } from '../../../../../shared/status-bar-usage-format'
import { normalizeUsagePercentageDisplay } from '../../../../../shared/usage-percentage-display'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '../../../store'
import { useAppStore } from '../../../store/index'
import { Button } from '../../ui/button'
import { Input } from '../../ui/input'
import {
buildUsageFormatValues,
renderUsageFormatTemplate
} from '../../status-bar/usage-format-template'
} from '../../status-bar/usage/usage-format-template'
import { getStatusBarUsageFormatEntry } from './appearance-status-bar-usage-format-search'
import { SettingsRow } from '../form-controls/SettingsFormControls'
@@ -13,7 +13,7 @@ import { ManageSessionsTable } from './ManageSessionsTable'
import {
notifyDaemonSessionInventoryInvalidated,
subscribeDaemonSessionInventoryInvalidated
} from '../../../status-bar/daemon-session-inventory-invalidation'
} from '../../../status-bar/resources/inventory/daemon-session-inventory-invalidation'
import { MANAGE_SESSIONS_SECTION_ID } from './open-terminal-manage-sessions'
import { TerminalTccAttributionNotice } from '../../developer-permissions/TerminalTccAttributionNotice'
import { translate } from '@/i18n/i18n'
@@ -1,4 +1,4 @@
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import type { AppState } from '@/store/types'
import {
closeTerminalTab,
@@ -10,7 +10,7 @@ import {
type TerminalTabRetirementState
} from '@/store/terminals/retirement/terminal-tab-retirement'
import { reserveTerminalRetirementTeardowns } from '@/store/terminals/retirement/terminal-retirement-teardown-reservation'
import { notifyDaemonSessionInventoryInvalidated } from '../status-bar/daemon-session-inventory-invalidation'
import { notifyDaemonSessionInventoryInvalidated } from '../status-bar/resources/inventory/daemon-session-inventory-invalidation'
const CLOSE_BATCH_SIZE = 2
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { CalendarClock, ExternalLink, RefreshCw, Sparkles } from 'lucide-react'
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '../../store'
import { useAppStore } from '../../store/index'
import { Button } from '../ui/button'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { StatCard } from './StatCard'
@@ -24,7 +24,7 @@ import {
normalizeComputerAwakeMode,
type ComputerAwakeStatus
} from '../../../../shared/computer-awake-mode'
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from './status-bar-context-menu-policy'
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from './composition/status-bar-context-menu-policy'
const INACTIVE_STATUS: ComputerAwakeStatus = {
mode: 'off',
@@ -13,7 +13,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useAppStore } from '../../store'
import { useAppStore } from '../../store/index'
import { BUNDLED_PET, BUNDLED_PETS, findBundledPet, isBundledPetId } from '../pet/pet-models'
import { PET_SIZE_MAX, PET_SIZE_MIN } from '../../../../shared/pet-types'
import { useTranslate } from '@/i18n/use-translate'
@@ -1,30 +1,4 @@
import React from 'react'
import { StatusBarSurface } from './StatusBarSurface'
export {
buildCodexStatusSwitchGroups,
getCodexStatusActiveId,
normalizeCodexStatusRuntimeTarget,
resolveCodexStatusAccountState
} from './status-bar-codex-accounts'
export {
buildClaudeStatusSwitchGroups,
getClaudeStatusActiveId,
normalizeClaudeStatusRuntimeTarget,
resolveClaudeStatusAccountState
} from './status-bar-claude-accounts'
export {
getStatusBarPreferredWslDistro,
type ClaudeStatusSwitchGroup,
type ClaudeStatusSwitchTarget,
type CodexStatusRuntimeTarget,
type CodexStatusSwitchGroup,
type CodexStatusSwitchTarget
} from './status-bar-runtime-targets'
export { ClaudeSwitcherMenu } from './ClaudeSwitcherMenu'
export { CodexSwitcherMenu } from './CodexSwitcherMenu'
export { InlineUsageBars } from './InlineProviderUsage'
export { ProviderDetailsMenu } from './ProviderDetailsMenu'
export { ProviderSegment } from './StatusBarProviderSegment'
export const StatusBar = React.memo(StatusBarSurface)
@@ -9,51 +9,55 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useTranslate } from '@/i18n/use-translate'
import { UsageRosterPanel } from './UsageRosterPanel'
import { UsageRosterPanel } from './usage/UsageRosterPanel'
import {
STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS,
shouldOpenStatusBarContextMenu
} from './status-bar-context-menu-policy'
import { StatusBarUsageEmptyCta } from './StatusBarUsageEmptyCta'
import { StatusBarUsageChangeNotices } from './StatusBarUsageChangeNotices'
import { UpdateStatusSegment } from './UpdateStatusSegment'
import { SkillUpdateStatusSegment } from './SkillUpdateStatusSegment'
} from './composition/status-bar-context-menu-policy'
import { StatusBarUsageEmptyCta } from './usage/StatusBarUsageEmptyCta'
import { StatusBarUsageChangeNotices } from './usage/StatusBarUsageChangeNotices'
import { UpdateStatusSegment } from './updates/UpdateStatusSegment'
import { SkillUpdateStatusSegment } from './updates/SkillUpdateStatusSegment'
import { NativeChatResumeStatusSegment } from './NativeChatResumeStatusSegment'
import { CaffeinateStatusSegment } from './CaffeinateStatusSegment'
import { PluginsStatusSegment } from './PluginsStatusSegment'
import { PluginStatusBarItems } from './PluginStatusBarItems'
import { RemoteServerUpdateStatusSegment } from './RemoteServerUpdateStatusSegment'
import { PluginsStatusSegment } from './plugins/PluginsStatusSegment'
import { PluginStatusBarItems } from './plugins/PluginStatusBarItems'
import { RemoteServerUpdateStatusSegment } from './updates/RemoteServerUpdateStatusSegment'
import { TOGGLE_FLOATING_TERMINAL_EVENT } from '@/components/floating-terminal/floating-terminal'
import { FloatingTerminalIconContextMenu } from '@/components/floating-terminal/FloatingTerminalIconContextMenu'
import { ClaudeSwitcherMenu } from './ClaudeSwitcherMenu'
import { CodexSwitcherMenu } from './CodexSwitcherMenu'
import { ProviderDetailsMenu } from './ProviderDetailsMenu'
import { AntigravityUsageMenu } from './AntigravityUsageMenu'
import { ClaudeSwitcherMenu } from './accounts/ClaudeSwitcherMenu'
import { CodexSwitcherMenu } from './accounts/CodexSwitcherMenu'
import { ProviderDetailsMenu } from './usage/ProviderDetailsMenu'
import { AntigravityUsageMenu } from './usage/AntigravityUsageMenu'
import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from '@/lib/overlay-interaction/close-all-context-menus'
import { ProviderSegment, UsageOverflowChip, getUsageTone } from './StatusBarProviderSegment'
import { ProviderSegment, UsageOverflowChip, getUsageTone } from './usage/StatusBarProviderSegment'
import {
UsageUnitLabel,
getUsageUnitLabelState,
usageChipShowsPercentage
} from './usage-unit-label'
import { useStatusBarController } from './use-status-bar-controller'
import { isOwnerNotSetUpPlaceholder } from './owner-usage-rate-limits'
import { StatusBarVisibilityMenu } from './StatusBarVisibilityMenu'
} from './usage/usage-unit-label'
import { useStatusBarController } from './composition/use-status-bar-controller'
import { isOwnerNotSetUpPlaceholder } from './usage/owner-usage-rate-limits'
import { StatusBarVisibilityMenu } from './composition/StatusBarVisibilityMenu'
import { isPairedWebClientWindow } from '@/app-shell/chrome/desktop-window-chrome'
const PetStatusSegment = lazyWithRetry(() =>
import('./PetStatusSegment').then((module) => ({ default: module.PetStatusSegment }))
)
const ResourceUsageStatusSegment = lazyWithRetry(() =>
import('./ResourceUsageStatusSegment').then((module) => ({
import('./resources/ResourceUsageStatusSegment').then((module) => ({
default: module.ResourceUsageStatusSegment
}))
)
const PortsStatusSegment = lazyWithRetry(() =>
import('./PortsStatusSegment').then((module) => ({ default: module.PortsStatusSegment }))
import('./host-connections/PortsStatusSegment').then((module) => ({
default: module.PortsStatusSegment
}))
)
const SshStatusSegment = lazyWithRetry(() =>
import('./SshStatusSegment').then((module) => ({ default: module.SshStatusSegment }))
import('./host-connections/SshStatusSegment').then((module) => ({
default: module.SshStatusSegment
}))
)
export type StatusBarProps = {
@@ -1,126 +0,0 @@
// @vitest-environment happy-dom
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { StatusBarUsageChangeNotices } from './StatusBarUsageChangeNotices'
const state = {
persistedUIReady: true,
statusBarCompactChangeNoticeDismissed: false,
dismissStatusBarCompactChangeNotice: vi.fn(),
statusBarUsageMode: 'compact',
statusBarVisible: true,
activeModal: 'none',
usagePercentageDisplayChangeNoticeDismissed: true,
dismissUsagePercentageDisplayChangeNotice: vi.fn()
}
vi.mock('@/store', () => ({
useAppStore: (selector: (store: typeof state) => unknown) => selector(state)
}))
describe('status bar usage change notices', () => {
let container: HTMLDivElement
let root: Root
beforeEach(() => {
vi.useFakeTimers()
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
vi.stubGlobal(
'ResizeObserver',
class {
observe(): void {}
unobserve(): void {}
disconnect(): void {}
}
)
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
x: 24,
y: 700,
top: 700,
left: 24,
bottom: 724,
right: 200,
width: 176,
height: 24,
toJSON: () => ({})
})
Object.assign(state, {
persistedUIReady: true,
statusBarCompactChangeNoticeDismissed: false,
statusBarUsageMode: 'compact',
statusBarVisible: true,
activeModal: 'none',
usagePercentageDisplayChangeNoticeDismissed: true
})
state.dismissStatusBarCompactChangeNotice.mockReset()
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
})
afterEach(() => {
act(() => root.unmount())
container.remove()
vi.useRealTimers()
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
function render(
hasVisibleUsageMeters = true,
children: ReactNode = <button>Usage</button>
): void {
act(() => {
root.render(
<StatusBarUsageChangeNotices hasVisibleUsageMeters={hasVisibleUsageMeters}>
{children}
</StatusBarUsageChangeNotices>
)
})
}
function settle(): void {
act(() => vi.advanceTimersByTime(1_800))
}
it.each(['verbose', 'compact'])('preserves the open menu and focus when choosing %s', (mode) => {
const menu = (
<DropdownMenu defaultOpen modal={false}>
<DropdownMenuTrigger asChild>
<button>Usage</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<button
onClick={() => {
state.statusBarUsageMode = mode
state.statusBarCompactChangeNoticeDismissed = true
}}
>
Choose mode
</button>
</DropdownMenuContent>
</DropdownMenu>
)
render(true, menu)
settle()
const trigger = container.querySelector('button')
const content = document.querySelector('[role="menu"]')
const selector = content?.querySelector('button')
expect(selector).toBeTruthy()
selector?.focus()
act(() => selector?.click())
render(true, menu)
expect(document.querySelector('[role="status"]')).toBeNull()
expect(container.querySelector('button')).toBe(trigger)
expect(document.querySelector('[role="menu"]')).toBe(content)
expect(document.activeElement).toBe(selector)
})
})
@@ -1,60 +0,0 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { AccountSwitchList } from './AccountSwitchList'
import { AccountSwitchRowItem } from './AccountSwitchRowItem'
import { InlineUsageSignInAction } from './InlineProviderUsage'
afterEach(cleanup)
it('keeps switching and signing in independent through keyboard and pointer selection', async () => {
const switchAccount = vi.fn()
const signIn = vi.fn()
render(
<DropdownMenu defaultOpen modal={false}>
<DropdownMenuTrigger>Accounts</DropdownMenuTrigger>
<DropdownMenuContent>
<AccountSwitchList heading="Codex Account">
<AccountSwitchRowItem
label="account@example.com"
active={false}
onSelect={(event) => {
event.preventDefault()
switchAccount()
}}
action={
<InlineUsageSignInAction
accountLabel="account@example.com"
isSigningIn={false}
disabled={false}
onSignIn={signIn}
/>
}
/>
</AccountSwitchList>
</DropdownMenuContent>
</DropdownMenu>
)
const account = await screen.findByRole('menuitem', { name: 'account@example.com' })
account.focus()
fireEvent.keyDown(account, { key: 'Enter' })
expect(switchAccount).toHaveBeenCalledTimes(1)
expect(signIn).not.toHaveBeenCalled()
fireEvent.keyDown(account, { key: 'ArrowDown' })
const signInAction = screen.getByRole('menuitem', { name: 'Sign in: account@example.com' })
await waitFor(() => expect(document.activeElement).toBe(signInAction))
fireEvent.keyDown(signInAction, { key: 'Enter' })
expect(signIn).toHaveBeenCalledTimes(1)
expect(switchAccount).toHaveBeenCalledTimes(1)
fireEvent.click(signInAction)
expect(signIn).toHaveBeenCalledTimes(2)
expect(switchAccount).toHaveBeenCalledTimes(1)
expect(screen.getByRole('menu')).toBeTruthy()
})
@@ -6,8 +6,8 @@ import {
DropdownMenuSeparator
} from '@/components/ui/dropdown-menu'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '../../store'
import { normalizeUsagePercentageDisplay } from '../../../../shared/usage-percentage-display'
import { useAppStore } from '../../../store/index'
import { normalizeUsagePercentageDisplay } from '../../../../../shared/usage-percentage-display'
export function AccountSwitchList({
heading,
@@ -1,6 +1,6 @@
import { defineFixture } from '../../component-fixtures/component-fixture'
import { defineFixture } from '../../../component-fixtures/component-fixture'
import { setRendererUiLanguage } from '@/i18n/i18n'
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
import type { ProviderRateLimits } from '../../../../../shared/rate-limit-types'
import {
DropdownMenu,
DropdownMenuContent,
@@ -8,7 +8,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { AccountSwitchList } from './AccountSwitchList'
import { AccountSwitchRowItem } from './AccountSwitchRowItem'
import { InlineUsageBars } from './InlineProviderUsage'
import { InlineUsageBars } from '../usage/InlineProviderUsage'
function accountRows(provider: 'codex' | 'claude', width = 520, language: 'en' | 'ru' = 'en') {
const limits: ProviderRateLimits = {
@@ -3,7 +3,7 @@ import type { ClaudeSwitcherMenuProps } from './ClaudeSwitcherMenu'
import { useProviderAccountList } from './use-provider-account-list'
import { providerChatAccountControl } from './ProviderAccountChatButton'
import React, { useCallback, useRef, useState } from 'react'
import { useAppStore } from '../../store'
import { useAppStore } from '../../../store/index'
import type { AccountOwner } from '@/runtime/provider-accounts/ownership/wsl-host-account-owner'
import { runtimeTargetEnvironmentId } from '@/runtime/rpc/targets/runtime-client-target'
import {
@@ -11,7 +11,7 @@ import {
emptyClaudeAccountsState
} from '@/runtime/provider-accounts/observations/runtime-provider-accounts-client'
import { toast } from 'sonner'
import { getClaudeAccountErrorDescription } from '../../lib/provider-accounts/provider-account-action-errors'
import { getClaudeAccountErrorDescription } from '../../../lib/provider-accounts/provider-account-action-errors'
import { useTranslate } from '@/i18n/use-translate'
import {
getWindowsTerminalCapabilityOwnerKey,
@@ -33,15 +33,15 @@ import {
resolveClaudeStatusAccountState
} from './status-bar-claude-accounts'
import { handleRunningClaudeSessionsAfterSwitch } from '@/lib/provider-accounts/claude-account-switch-sessions'
import { AccountRuntimeToggle } from './StatusBarAccountControls'
import { AccountRuntimeToggle } from '../composition/StatusBarAccountControls'
import {
InlineUsageBars,
InlineUsageSignInAction,
InlineUsageSkeleton
} from './InlineProviderUsage'
} from '../usage/InlineProviderUsage'
import { AccountSwitchSection, AccountSwitchSignedOutHeading } from './AccountSwitchList'
import { AccountSwitchRowItem } from './AccountSwitchRowItem'
import { ProviderDetailsMenu } from './ProviderDetailsMenu'
import { ProviderDetailsMenu } from '../usage/ProviderDetailsMenu'
import { signInToClaudeAccount } from '@/lib/provider-accounts/claude-account-sign-in'
// Retain account switch/reset drill-in even though the footer now opens UsageRosterPanel.
@@ -1,7 +1,7 @@
import type React from 'react'
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
import type { ProviderRateLimits } from '../../../../../shared/rate-limit-types'
import type { ChatAccountAction } from './ProviderAccountChatButton'
import { useAppStore } from '../../store'
import { useAppStore } from '../../../store/index'
import { defaultScopeHost } from '@/runtime/hosts/selection/default-creation-host'
import { useActiveAccountOwner } from '@/runtime/provider-accounts/ownership/use-active-account-owner'
import { ClaudeSwitcherForOwner } from './ClaudeSwitcherForOwner'
@@ -11,7 +11,7 @@ import {
DialogTitle
} from '@/components/ui/dialog'
import { useTranslate } from '@/i18n/use-translate'
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from './status-bar-context-menu-policy'
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from '../composition/status-bar-context-menu-policy'
export function CodexResetCredits({
resetCreditCount,
@@ -1,12 +1,12 @@
import { defineFixture } from '../../component-fixtures/component-fixture'
import { getDefaultSettings } from '../../../../shared/constants'
import type { CodexManagedAccount } from '../../../../shared/managed-account-types'
import { createEmptyRateLimitState } from '../../../../shared/rate-limit-state-factory'
import { defineFixture } from '../../../component-fixtures/component-fixture'
import { getDefaultSettings } from '../../../../../shared/constants'
import type { CodexManagedAccount } from '../../../../../shared/managed-account-types'
import { createEmptyRateLimitState } from '../../../../../shared/rate-limit-state-factory'
import type {
InactiveAccountUsage,
ProviderRateLimits,
RateLimitWindow
} from '../../../../shared/rate-limit-types'
} from '../../../../../shared/rate-limit-types'
import { makeTab } from '@/store/terminals/testing/terminal-tab.test-fixture'
import { CodexSwitcherMenu } from './CodexSwitcherMenu'
import {
@@ -1,16 +1,19 @@
import React from 'react'
import { CodexResetCredits, CodexResetConfirmation } from './CodexResetCredits'
import { DropdownMenuItem, DropdownMenuLabel } from '@/components/ui/dropdown-menu'
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
import type { ProviderRateLimits } from '../../../../../shared/rate-limit-types'
import { useTranslate } from '@/i18n/use-translate'
import { AccountRuntimeToggle, CodexRestartStatusPrompt } from './StatusBarAccountControls'
import {
AccountRuntimeToggle,
CodexRestartStatusPrompt
} from '../composition/StatusBarAccountControls'
import {
InlineUsageBars,
InlineUsageSignInAction,
InlineUsageSkeleton,
isUnavailableInactiveUsage
} from './InlineProviderUsage'
import { ProviderDetailsMenu } from './ProviderDetailsMenu'
} from '../usage/InlineProviderUsage'
import { ProviderDetailsMenu } from '../usage/ProviderDetailsMenu'
import { AccountSwitchSection } from './AccountSwitchList'
import { AccountSwitchRowItem } from './AccountSwitchRowItem'
import { useCodexSwitcherController } from './use-codex-switcher-controller'
@@ -1,8 +1,8 @@
import type { ComponentProps } from 'react'
import { Button } from '@/components/ui/button'
import { useTranslate } from '@/i18n/use-translate'
import { useAppStore } from '@/store'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import { useAppStore } from '@/store/index'
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
export type ChatAccountAction = { target: RuntimeClientTarget; canSwitch: () => Promise<boolean> }
@@ -1,10 +1,10 @@
import React from 'react'
import { Card } from '@/components/ui/card'
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
import type { UsagePercentageDisplay } from '../../../../shared/usage-percentage-display'
import type { ProviderRateLimits } from '../../../../../shared/rate-limit-types'
import type { UsagePercentageDisplay } from '../../../../../shared/usage-percentage-display'
import { useTranslate } from '@/i18n/use-translate'
import { getProviderDisplayName } from '../../../../shared/usage-provider-windows'
import { formatTimeAgo, ProviderIcon, ProviderPanel } from './tooltip'
import { getProviderDisplayName } from '../../../../../shared/usage-provider-windows'
import { formatTimeAgo, ProviderIcon, ProviderPanel } from '../usage/provider-usage-panel'
export function ProviderIdentityCard({
p,
@@ -1,7 +1,7 @@
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
import { toast } from 'sonner'
import type { CodexRateLimitAccountsState } from '../../../../shared/managed-account-types'
import type { AppState } from '../../store/types'
import type { CodexRateLimitAccountsState } from '../../../../../shared/managed-account-types'
import type { AppState } from '../../../store/types'
import { translate } from '@/i18n/i18n'
import {
markLiveCodexSessionsForRestart,
@@ -1,8 +1,6 @@
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
import { formatResetCreditExpiry } from './tooltip'
import type { Translate } from '../../i18n/use-translate'
export { getCodexAccountSyncKey } from './provider-account-sync-key'
import type { ProviderRateLimits } from '../../../../../shared/rate-limit-types'
import { formatResetCreditExpiry } from '../usage/provider-usage-panel'
import type { Translate } from '../../../i18n/use-translate'
export function getCodexResetProjection(
codex: ProviderRateLimits,
@@ -1,6 +1,6 @@
import { sameProviderAccountData } from '@/runtime/provider-accounts/observations/observed-provider-accounts'
import type { ClaudeAccountSettings } from './status-bar-claude-accounts'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
// Cache by settings identity because Zustand reevaluates selectors on every store write and roster serialization is expensive.
const claudeKeyBySettings = new WeakMap<GlobalSettings, string>()
@@ -1,5 +1,5 @@
import React from 'react'
import minimaxIconUrl from '../../../../../resources/minimax-icon.svg?url'
import minimaxIconUrl from '../../../../../../resources/minimax-icon.svg?url'
export function OpenAIIcon({
size = 14,
@@ -1,5 +1,5 @@
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { ClaudeRateLimitAccountsState } from '../../../../shared/managed-account-types'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import type { ClaudeRateLimitAccountsState } from '../../../../../shared/managed-account-types'
import { translate } from '@/i18n/i18n'
import {
getCodexStatusRuntimeKey,
@@ -1,5 +1,5 @@
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { CodexRateLimitAccountsState } from '../../../../shared/managed-account-types'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import type { CodexRateLimitAccountsState } from '../../../../../shared/managed-account-types'
import { translate } from '@/i18n/i18n'
import {
getCodexAccountDisplayDetail,
@@ -1,9 +1,9 @@
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { RateLimitRuntimeTarget } from '../../../../shared/rate-limit-types'
import { resolveLocalAccountRuntimeTarget } from '../../../../shared/local-account-runtime'
import { getRendererAppPlatform } from '../../app-shell/platform/renderer-app-platform'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import type { RateLimitRuntimeTarget } from '../../../../../shared/rate-limit-types'
import { resolveLocalAccountRuntimeTarget } from '../../../../../shared/local-account-runtime'
import { getRendererAppPlatform } from '../../../app-shell/platform/renderer-app-platform'
import { translate } from '@/i18n/i18n'
import { getHostRuntimeLabel } from '../../lib/provider-accounts/account-runtime-label'
import { getHostRuntimeLabel } from '../../../lib/provider-accounts/account-runtime-label'
export type CodexStatusRuntimeTarget = {
runtime: 'host' | 'wsl'
@@ -1,4 +1,4 @@
import { useAppStore } from '../../store'
import { useAppStore } from '../../../store/index'
import type { ChatAccountAction } from './ProviderAccountChatButton'
import {
getClaudeAccountSettings,
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import type { CodexRateLimitAccountsState } from '../../../../shared/managed-account-types'
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
import { useAppStore } from '../../store'
import type { CodexRateLimitAccountsState } from '../../../../../shared/managed-account-types'
import type { ProviderRateLimits } from '../../../../../shared/rate-limit-types'
import { useAppStore } from '../../../store/index'
import { defaultScopeHost } from '@/runtime/hosts/selection/default-creation-host'
import { useActiveAccountOwner } from '@/runtime/provider-accounts/ownership/use-active-account-owner'
import { runtimeTargetEnvironmentId } from '@/runtime/rpc/targets/runtime-client-target'
@@ -18,7 +18,8 @@ import {
getWindowsTerminalCapabilityOwnerKey,
useWindowsTerminalCapabilities
} from '@/runtime/hosts/capabilities/windows-terminal-capabilities'
import { getCodexAccountSyncKey, getCodexResetProjection } from './codex-switcher-projection'
import { getCodexResetProjection } from './codex-switcher-projection'
import { getCodexAccountSyncKey } from './provider-account-sync-key'
import {
getCodexStatusRuntimeKey,
getStatusBarPreferredWslDistro,
@@ -5,7 +5,7 @@ import {
type ProviderAccountsSnapshot
} from '@/runtime/provider-accounts/observations/runtime-provider-accounts-client'
import { observedProviderAccounts } from '@/runtime/provider-accounts/observations/observed-provider-accounts'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
const noSubscription = () => () => {}
const noSnapshot = () => null
@@ -1,49 +0,0 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { CodexRestartStatusPrompt } from './StatusBarAccountControls'
const { state } = vi.hoisted(() => ({
state: {
tabsByWorktree: { workspace: [{ id: 'tab-1' }] },
ptyIdsByTabId: { 'tab-1': ['pty-1'] },
codexRestartNoticeByPtyId: {
'pty-1': { previousAccountLabel: 'old', nextAccountLabel: 'new' }
},
queuePaneAccountRestarts: vi.fn()
}
}))
vi.mock('../../store', () => ({
useAppStore: (selector: (value: typeof state) => unknown) => selector(state)
}))
afterEach(cleanup)
describe('Codex restart banner keyboard action', () => {
it('reaches the restart action with arrow keys and activates it once with Enter', async () => {
render(
<DropdownMenu defaultOpen modal={false}>
<DropdownMenuTrigger>Accounts</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Previous action</DropdownMenuItem>
<CodexRestartStatusPrompt />
</DropdownMenuContent>
</DropdownMenu>
)
const previous = await screen.findByRole('menuitem', { name: 'Previous action' })
previous.focus()
fireEvent.keyDown(previous, { key: 'ArrowDown' })
const restart = screen.getByRole('menuitem', { name: 'Restart Session' })
await waitFor(() => expect(document.activeElement).toBe(restart))
fireEvent.keyDown(restart, { key: 'Enter' })
expect(state.queuePaneAccountRestarts).toHaveBeenCalledExactlyOnceWith(['pty-1'], 'codex')
expect(screen.getByRole('menuitem', { name: 'Restart Session' })).toBeTruthy()
})
})
@@ -2,8 +2,8 @@ import { TriangleAlert } from 'lucide-react'
import React, { useMemo } from 'react'
import { Button } from '@/components/ui/button'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { useAppStore } from '../../store'
import { summarizeCodexRestartStatus } from './codex-restart-status-summary'
import { useAppStore } from '../../../store/index'
import { summarizeCodexRestartStatus } from '../accounts/codex-restart-status-summary'
import { useTranslate } from '@/i18n/use-translate'
export function CodexRestartStatusPrompt(): React.JSX.Element | null {
@@ -7,7 +7,13 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { AgentIcon } from '@/components/agent/icons/AgentIcon'
import { ClaudeIcon, GeminiIcon, MiniMaxIcon, OpenAIIcon, OpenCodeGoIcon } from './icons'
import {
ClaudeIcon,
GeminiIcon,
MiniMaxIcon,
OpenAIIcon,
OpenCodeGoIcon
} from '../accounts/provider-icons'
import { useTranslate } from '@/i18n/use-translate'
import { isStatusBarItemAvailable } from './status-bar-agent-gating'
import type { StatusBarController } from './use-status-bar-controller'
@@ -1,5 +1,5 @@
import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent'
import type { StatusBarItem } from '../../../../shared/ui-chrome-types'
import type { TuiAgent } from '../../../../../shared/agent-launch/catalog/tui-agent'
import type { StatusBarItem } from '../../../../../shared/ui-chrome-types'
// Hide missing CLI items and toggles after detection; unknown detection preserves them to avoid cold-start flicker.
const CLI_GATED_ITEMS: ReadonlySet<StatusBarItem> = new Set([
@@ -1,6 +1,6 @@
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { hasUsageData } from '../../../../shared/usage-provider-windows'
import type { ProviderRateLimits } from '../../../../../shared/rate-limit-types'
import type { GlobalSettings } from '../../../../../shared/global-settings-types'
import { hasUsageData } from '../../../../../shared/usage-provider-windows'
export type UsageProviderSettings = Pick<
GlobalSettings,
@@ -1,10 +1,10 @@
import type { ProviderRateLimits, RateLimitWindow } from '../../../../shared/rate-limit-types'
import type { StatusBarUsageMode } from '../../../../shared/status-bar-usage-mode'
import { isCursorUsageBucket } from '../../../../shared/cursor-usage-buckets'
import type { ProviderRateLimits, RateLimitWindow } from '../../../../../shared/rate-limit-types'
import type { StatusBarUsageMode } from '../../../../../shared/status-bar-usage-mode'
import { isCursorUsageBucket } from '../../../../../shared/cursor-usage-buckets'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { formatRateLimitWindowChipLabel } from '../../../../shared/rate-limit-window-label'
import { getUsageHeadlineSection } from './UsageRosterPanel'
import { formatRateLimitWindowChipLabel } from '../../../../../shared/rate-limit-window-label'
import { getUsageHeadlineSection } from '../usage/UsageRosterPanel'
export type StatusBarUsageSection = {
key: string
@@ -1,5 +1,5 @@
import { useAppStore } from '../../store'
import type { StatusBarItem } from '../../../../shared/ui-chrome-types'
import { useAppStore } from '../../../store/index'
import type { StatusBarItem } from '../../../../../shared/ui-chrome-types'
import { isStatusBarItemAvailable } from './status-bar-agent-gating'
/** Keep CLI toggles visible until detection proves their agent missing. */
@@ -1,21 +1,24 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
import { useAppStore } from '../../store/index'
import { selectFloatingWorkspaceHasUnread } from '../../store/workspaces/floating/floating-workspace-unread-selector'
import type { ProviderRateLimits } from '../../../../shared/rate-limit-types'
import { normalizeUsagePercentageDisplay } from '../../../../shared/usage-percentage-display'
import { normalizeStatusBarUsageMode } from '../../../../shared/status-bar-usage-mode'
import { useAppStore } from '../../../store/index'
import { selectFloatingWorkspaceHasUnread } from '../../../store/workspaces/floating/floating-workspace-unread-selector'
import type { ProviderRateLimits } from '../../../../../shared/rate-limit-types'
import { normalizeUsagePercentageDisplay } from '../../../../../shared/usage-percentage-display'
import { normalizeStatusBarUsageMode } from '../../../../../shared/status-bar-usage-mode'
import { isStatusBarItemAvailable } from './status-bar-agent-gating'
import { getVisibleUsageProvider, isUsageEmptyState } from './status-bar-provider-visibility'
import {
getUsageProviderAccountsSectionId,
usageRowSignInOpensSettings
} from './usage-provider-settings-target'
import { useStatusBarMenuFocusHandoff } from './ProviderDetailsMenu'
} from '../usage/usage-provider-settings-target'
import { useStatusBarMenuFocusHandoff } from '../usage/ProviderDetailsMenu'
import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from '@/lib/overlay-interaction/close-all-context-menus'
import { useStatusBarDensity } from './status-bar-density'
import { getUsageOwnerHostScope, useOwnerUsageRateLimits } from './use-owner-usage-rate-limits'
import { isOwnerNotSetUpPlaceholder } from './owner-usage-rate-limits'
import {
getUsageOwnerHostScope,
useOwnerUsageRateLimits
} from '../usage/use-owner-usage-rate-limits'
import { isOwnerNotSetUpPlaceholder } from '../usage/owner-usage-rate-limits'
import { useTranslate } from '@/i18n/use-translate'
export function useStatusBarController(floatingTerminalOpen: boolean) {
@@ -2,20 +2,20 @@ import React, { useCallback, useMemo, useState } from 'react'
import { Plug, ChevronDown, ChevronRight, LoaderCircle } from 'lucide-react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import { refreshWorkspacePortScanForTarget } from '@/lib/workspace-ports/workspace-port-scan-refresh'
import { useWorktreeRuntimeTarget } from '@/runtime/rpc/targets/use-worktree-runtime-target'
import {
getUnavailableWorkspacePortHosts,
type WorkspacePortHostRef
} from '@/lib/workspace-ports/workspace-port-host-availability'
import { getLocalExecutionHostLabel } from '../../../../shared/execution-host'
import { getLocalExecutionHostLabel } from '../../../../../shared/execution-host'
import {
getExternalWorkspacePorts,
getWorkspacePortGroups
} from '@/lib/workspace-ports/workspace-port-groups'
import { SelectedTextCopyMenu } from '@/components/SelectedTextCopyMenu'
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from './status-bar-context-menu-policy'
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from '../composition/status-bar-context-menu-policy'
import { PortRow, WorkspaceGroupRows } from './ports-status-popover-rows'
import { useTranslate } from '@/i18n/use-translate'
@@ -14,8 +14,8 @@ import '@/runtime/hosts/connections/runtime-host-connection-state'
import {
isConnectedRuntimeHostState,
type RuntimeHostConnectionState
} from '../../../../shared/remote-runtime/host-status/runtime-host-connection-state'
import type { RemoteRuntimeSharedConnectionDiagnostics } from '../../../../shared/remote-runtime/control/remote-runtime-shared-control-types'
} from '../../../../../shared/remote-runtime/host-status/runtime-host-connection-state'
import type { RemoteRuntimeSharedConnectionDiagnostics } from '../../../../../shared/remote-runtime/control/remote-runtime-shared-control-types'
import type {
RuntimeHostBlockedCopy,
RuntimeHostBlockedRecovery
@@ -9,8 +9,8 @@ import {
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
import { cn } from '@/lib/class-names'
import { canConnectSshStatus } from '@/ssh/ssh-connection-recoverability'
import { STATUS_LABELS, statusColor } from '../settings/ssh/targets/SshTargetCard'
import { HostListOverflowNote } from '../host-picker/HostListOverflowNote'
import { STATUS_LABELS, statusColor } from '../../settings/ssh/targets/SshTargetCard'
import { HostListOverflowNote } from '../../host-picker/HostListOverflowNote'
import { findSshHostMatches, type SshStatusMenuTarget } from './ssh-status-menu-rows'
import {
moreSshHostMatchesLabel,
@@ -8,27 +8,27 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useAppStore } from '../../store/index'
import { useAppStore } from '../../../store/index'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides'
import { getHostDisplayLabelOverrides } from '../../../../../shared/host-setting-overrides'
import {
isOwnedSshTargetId,
toRuntimeExecutionHostId,
toSshExecutionHostId
} from '../../../../shared/execution-host'
import { buildExecutionHostRegistry } from '../../../../shared/execution-host/execution-host-registry'
import { pickerExecutionHosts } from '../../../../shared/execution-host/managed-orcad-execution-host'
} from '../../../../../shared/execution-host'
import { buildExecutionHostRegistry } from '../../../../../shared/execution-host/execution-host-registry'
import { pickerExecutionHosts } from '../../../../../shared/execution-host/managed-orcad-execution-host'
import {
isListedRemoteServer,
runtimePairingRepairRoute
} from '../../../../shared/remote-runtime/host-catalog/runtime-environments'
} from '../../../../../shared/remote-runtime/host-catalog/runtime-environments'
import { RuntimeHostStatusRow } from './RuntimeHostStatusRow'
import { connectedHostCountLabel, moreSshHostsLabel } from './ssh-status-segment-copy'
import { SshHostFindPopover } from './SshHostFindPopover'
import { splitSshStatusMenuRows } from './ssh-status-menu-rows'
import { SshTargetStatusRow } from './SshTargetStatusRow'
import { syncSshConfigTargetsPassively } from '../../runtime/hosts/ssh/ssh-config-passive-sync'
import { syncSshConfigTargetsPassively } from '../../../runtime/hosts/ssh/ssh-config-passive-sync'
import { connectRuntimeHostAndReloadProjects } from './runtime-environment-explicit-connect'
import {
runtimeHostBlockedCopy,
@@ -39,9 +39,9 @@ import {
} from './remote-host-connection-status'
import type { RuntimeHostBlockedRecovery } from './remote-host-connection-status'
import { runtimeHostConnectionStateForEntry } from '@/runtime/hosts/connections/runtime-host-connection-state'
import { isConnectedRuntimeHostState } from '../../../../shared/remote-runtime/host-status/runtime-host-connection-state'
import { RuntimeRepairDialog } from '../runtime-access/RuntimeRepairDialog'
import type { SshConnectionStatus } from '../../../../shared/ssh-types'
import { isConnectedRuntimeHostState } from '../../../../../shared/remote-runtime/host-status/runtime-host-connection-state'
import { RuntimeRepairDialog } from '../../runtime-access/RuntimeRepairDialog'
import type { SshConnectionStatus } from '../../../../../shared/ssh-types'
export function SshStatusSegment({
compact,
@@ -5,9 +5,9 @@ import { toastSshConnectFailure } from '@/ssh/ssh-connect-failure-toast'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
import { useMountedRef } from '@/hooks/useMountedRef'
import { useAppStore } from '../../store'
import { STATUS_LABELS, statusColor } from '../settings/ssh/targets/SshTargetCard'
import { formatSshUserFacingError } from '../settings/ssh/targets/ssh-user-facing-error'
import { useAppStore } from '../../../store/index'
import { STATUS_LABELS, statusColor } from '../../settings/ssh/targets/SshTargetCard'
import { formatSshUserFacingError } from '../../settings/ssh/targets/ssh-user-facing-error'
import { canConnectSshStatus } from '@/ssh/ssh-connection-recoverability'
import { sshConnectVerb } from '@/ssh/ssh-connect-verb'
import {
@@ -16,7 +16,7 @@ import {
isSshConnectInFlight,
useSshConnectInFlight
} from '@/ssh/ssh-connect-in-flight'
import type { SshConnectionStatus } from '../../../../shared/ssh-types'
import type { SshConnectionStatus } from '../../../../../shared/ssh-types'
export async function connectSshTargetFromStatusMenu(targetId: string): Promise<void> {
if (isSshConnectInFlight(targetId)) {
@@ -18,8 +18,8 @@ import {
import type { WorkspacePortGroup } from '@/lib/workspace-ports/workspace-port-groups'
import { useLocalhostLabelRouteForPort } from '@/lib/workspace-ports/workspace-port-localhost-label-selector'
import { useWorktreeRuntimeTarget } from '@/runtime/rpc/targets/use-worktree-runtime-target'
import { useAppStore } from '@/store'
import type { WorkspacePort } from '../../../../shared/workspace-ports'
import { useAppStore } from '@/store/index'
import type { WorkspacePort } from '../../../../../shared/workspace-ports'
import { translate } from '@/i18n/i18n'
import { useTranslate } from '@/i18n/use-translate'
@@ -1,17 +1,17 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { SshConnectionStatus } from '../../../../shared/ssh-types'
import type { RemoteRuntimeSharedConnectionDiagnostics } from '../../../../shared/remote-runtime/control/remote-runtime-shared-control-types'
import type { RuntimeHostBlockedReason } from '../../../../shared/remote-runtime/host-status/runtime-host-status'
import type { RuntimePairingRepairRoute } from '../../../../shared/remote-runtime/host-catalog/runtime-environments'
import type { RuntimeCompatVerdict } from '../../../../shared/protocol-compat'
import type { SshConnectionStatus } from '../../../../../shared/ssh-types'
import type { RemoteRuntimeSharedConnectionDiagnostics } from '../../../../../shared/remote-runtime/control/remote-runtime-shared-control-types'
import type { RuntimeHostBlockedReason } from '../../../../../shared/remote-runtime/host-status/runtime-host-status'
import type { RuntimePairingRepairRoute } from '../../../../../shared/remote-runtime/host-catalog/runtime-environments'
import type { RuntimeCompatVerdict } from '../../../../../shared/protocol-compat'
import { describeRuntimeCompatBlock } from '@/runtime/hosts/display/runtime-host-copy'
import '@/runtime/hosts/connections/runtime-host-connection-state'
import {
runtimeStatusForOverall,
type HostStatus,
type RuntimeHostConnectionState
} from '../../../../shared/remote-runtime/host-status/runtime-host-connection-state'
} from '../../../../../shared/remote-runtime/host-status/runtime-host-connection-state'
import { isConnectingSshStatus } from '@/ssh/ssh-connection-recoverability'
/** 'blocked' is a host that refused us (revoked pairing or incompatible versions). */
@@ -1,10 +1,10 @@
import { toast } from 'sonner'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { RuntimeStatus } from '../../../../shared/runtime-types'
import type { ExecutionHostId } from '../../../../../shared/execution-host'
import type { RuntimeStatus } from '../../../../../shared/runtime-session-contracts'
import { refreshRuntimeProjectWorktreesAndLineage } from '@/hooks/runtime-project-refresh-scheduler'
import { translate } from '@/i18n/i18n'
import { unwrapRuntimeRpcResult } from '@/runtime/rpc/calls/runtime-rpc-result'
import { useAppStore } from '../../store'
import { useAppStore } from '../../../store/index'
export async function connectRuntimeEnvironmentAndRecordStatus(
environmentId: string,
@@ -1,8 +1,11 @@
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { parseExecutionHostId, toRuntimeExecutionHostId } from '../../../../shared/execution-host'
import type { SessionMemory, WorktreeMemory } from '../../../../shared/process-stats-types'
import type { RuntimeHostResourceSample } from './resource-usage-merge-types'
import {
parseExecutionHostId,
toRuntimeExecutionHostId
} from '../../../../../shared/execution-host'
import type { SessionMemory, WorktreeMemory } from '../../../../../shared/process-stats-types'
import type { RuntimeHostResourceSample } from '../resources/workspace-attribution/resource-usage-merge-types'
export type RuntimeHostResourceResult =
| { status: 'sampled'; worktrees: WorktreeMemory[] }
@@ -1,5 +1,5 @@
import type { SshConnectionStatus } from '../../../../shared/ssh-types'
import { boundHostList, type BoundedHostList } from '../host-picker/bounded-host-list'
import type { SshConnectionStatus } from '../../../../../shared/ssh-types'
import { boundHostList, type BoundedHostList } from '../../host-picker/bounded-host-list'
// Bound idle hosts because generated SSH configs can turn thousands of rows into gigabytes of DOM.
export const SSH_STATUS_MENU_IDLE_ROW_LIMIT = 25
@@ -1,300 +0,0 @@
// @vitest-environment happy-dom
import type { ComponentProps } from 'react'
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type {
PtyFencedTerminationOutcome,
PtyFencedTerminationRequest
} from '../../../../shared/terminal/pty/lifecycle/pty-fenced-termination'
import { TooltipProvider } from '@/components/ui/tooltip'
import { ResourcePtyPanel } from './ResourcePtyPanel'
import { useResourcePtyActions } from './use-resource-pty-actions'
import { useResourceSessionInventory } from './use-resource-session-inventory'
import { makeRepo, makeWorktree } from '../worktree-jump/worktree-jump-palette-test-fixtures'
vi.mock('@/i18n/i18n', () => ({
getIntlLocale: () => 'en',
translate: (_key: string, fallback: string, values?: Record<string, string | number>) =>
Object.entries(values ?? {}).reduce(
(text, [key, value]) => text.replace(`{{${key}}}`, String(value)),
fallback
)
}))
const VIEWPORT_HEIGHT = 360
const ROW_HEIGHT = 40
beforeEach(() => {
vi.stubGlobal(
'ResizeObserver',
class {
observe() {}
unobserve() {}
disconnect() {}
}
)
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (
this: HTMLElement
) {
return this.hasAttribute('data-resource-pty-body') ? VIEWPORT_HEIGHT : ROW_HEIGHT
})
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(function (this: Element) {
return new DOMRect(
0,
0,
400,
this.hasAttribute('data-resource-pty-body') ? VIEWPORT_HEIGHT : ROW_HEIGHT
)
})
})
afterEach(() => {
cleanup()
vi.useRealTimers()
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
function panelProps(count: number): ComponentProps<typeof ResourcePtyPanel> {
return {
active: true,
inventory: {
sessionInventory: {
sessions: Array.from({ length: count }, (_, index) => ({
id: `pty-${String(index).padStart(5, '0')}`,
incarnationId: 'A',
fencedTermination: true,
cwd: '/notes',
title: `Terminal ${index}`,
worktreeId: 'wt-1',
agentOwnership: 'absent' as const
})),
count
},
observedAt: 1,
sessionsError: false,
loading: false,
refreshSessions: async () => {},
clearSessionsError: () => {},
invalidate: () => {},
removeSession: () => {},
removeSessions: () => {}
},
actions: {
ending: new Set(),
errors: new Map(),
end: () => {},
endMany: async () => {}
},
onNavigate: () => {},
bindings: { tabsByWorktree: {}, ptyIdsByTabId: {}, workspaceSessionReady: true },
worktrees: [makeWorktree('wt-1', 'Notes', { lastActivityAt: 100 })],
repos: [makeRepo()],
projectGroups: [],
runtimePaneTitlesByTabId: {}
}
}
it.each([1_000, 10_000])(
'bounds mounted PTY rows for %i sessions and reaches the last row',
(count) => {
const props = panelProps(count)
const view = render(
<TooltipProvider>
<ResourcePtyPanel {...props} />
</TooltipProvider>
)
const scroller = view.container.querySelector<HTMLElement>('[data-resource-pty-body]')
if (!scroller) {
throw new Error('PTY scroller missing')
}
const mountedRows = () => view.container.querySelectorAll('[data-resource-pty-row]')
expect(mountedRows().length).toBeGreaterThan(0)
expect(mountedRows().length).toBeLessThan(50)
expect(mountedRows()[0]?.getAttribute('data-resource-pty-row')).toContain('pty-00000')
expect(view.container.querySelector('[role="listitem"]')?.getAttribute('aria-setsize')).toBe(
String(count + 2)
)
act(() => {
scroller.scrollTop = (count + 2) * ROW_HEIGHT - VIEWPORT_HEIGHT
fireEvent.scroll(scroller)
})
expect(mountedRows().length).toBeGreaterThan(0)
expect(mountedRows().length).toBeLessThan(50)
expect(
mountedRows()[mountedRows().length - 1]?.getAttribute('data-resource-pty-row')
).toContain(`pty-${String(count - 1).padStart(5, '0')}`)
}
)
it('selects across virtualized rows, excludes unavailable PTYs, and drops replaced identities', () => {
const props = panelProps(1_000)
props.inventory.sessionInventory.sessions[1]!.fencedTermination = false
props.inventory.sessionInventory.sessions[2]!.exiting = true
props.actions.ending = new Set([JSON.stringify(['pty-00003', 'A'])])
const endMany = vi.fn()
props.actions.endMany = endMany
const panel = (value: typeof props) => (
<TooltipProvider>
<ResourcePtyPanel {...value} />
</TooltipProvider>
)
const view = render(panel(props))
const selectAll = view.getByRole('checkbox', { name: 'Select all terminals' })
const first = view.getByRole('checkbox', { name: 'Select Terminal 0' })
expect(view.getByRole('checkbox', { name: 'Select Terminal 1' }).hasAttribute('disabled')).toBe(
true
)
expect(view.getByRole('checkbox', { name: 'Select Terminal 2' }).hasAttribute('disabled')).toBe(
true
)
fireEvent.click(first)
expect(selectAll.getAttribute('aria-checked')).toBe('mixed')
fireEvent.click(selectAll)
fireEvent.click(view.getByRole('button', { name: 'End selected (997)' }))
expect(endMany).toHaveBeenCalledTimes(1)
expect(endMany.mock.calls[0]![0]).toHaveLength(997)
fireEvent.click(selectAll)
fireEvent.click(first)
view.rerender(
panel({
...props,
inventory: {
...props.inventory,
sessionInventory: {
...props.inventory.sessionInventory,
sessions: props.inventory.sessionInventory.sessions.map((session) => ({
...session,
incarnationId: 'B'
}))
}
}
})
)
expect(view.getByRole('button', { name: 'End selected (0)' }).hasAttribute('disabled')).toBe(true)
})
function LivePtyPanel({
active,
props
}: {
active: boolean
props: ComponentProps<typeof ResourcePtyPanel>
}) {
const inventory = useResourceSessionInventory(true, active, true)
const actions = useResourcePtyActions(inventory)
return (
<TooltipProvider>
<button>Elsewhere</button>
<ResourcePtyPanel {...props} active={active} inventory={inventory} actions={actions} />
</TooltipProvider>
)
}
async function livePanel(count: number) {
const rect = new DOMRect(0, 0, 400, VIEWPORT_HEIGHT)
vi.spyOn(Element.prototype, 'getClientRects').mockReturnValue(
Object.assign([rect], { item: () => rect })
)
const props = panelProps(count)
const list = vi.fn().mockResolvedValue(props.inventory.sessionInventory.sessions)
const finishes: ((outcome: PtyFencedTerminationOutcome) => void)[] = []
const kill = vi.fn<
(request: PtyFencedTerminationRequest) => Promise<PtyFencedTerminationOutcome>
>(
() =>
new Promise<PtyFencedTerminationOutcome>((resolve) => {
finishes.push(resolve)
})
)
vi.stubGlobal('api', {
pty: { listSessions: list, killFenced: kill, onSpawned: () => () => {}, onExit: () => () => {} }
})
const panel = (active: boolean) => <LivePtyPanel active={active} props={props} />
const view = render(panel(true))
await act(async () => {})
const body = view.container.querySelector<HTMLElement>('[data-resource-pty-body]')
if (!body) {
throw new Error('PTY scroller missing')
}
body.style.visibility = 'visible'
const finish = async (outcome: PtyFencedTerminationOutcome) => {
vi.useFakeTimers()
if (outcome.outcome === 'ended') {
const ended = new Set(kill.mock.calls.map(([request]) => request.id))
list.mockResolvedValue(
props.inventory.sessionInventory.sessions.filter((session) => !ended.has(session.id))
)
}
await act(async () => {
finishes.splice(0).forEach((resolve) => resolve(outcome))
await vi.advanceTimersByTimeAsync(100)
})
}
return {
...view,
body,
finish,
kill,
user: userEvent.setup(),
setActive: (active: boolean) => view.rerender(panel(active))
}
}
it.each(['ended', 'refused'] as const)(
'hands keyboard End focus to the PTY body before disabling a virtualized row, then handles %s',
async (outcome) => {
const view = await livePanel(1_000)
act(() => {
view.body.scrollTop = 1_002 * ROW_HEIGHT - VIEWPORT_HEIGHT
fireEvent.scroll(view.body)
})
const scrollTop = view.body.scrollTop
const end = view.getByRole('button', { name: 'End Terminal 999' })
end.focus()
await view.user.keyboard('{Enter}')
expect(view.kill).toHaveBeenCalledTimes(1)
expect(end.hasAttribute('disabled')).toBe(true)
expect(document.activeElement).toBe(view.body)
expect(view.body.scrollTop).toBe(scrollTop)
await view.finish(outcome === 'ended' ? { outcome } : { outcome, reason: 'claimed' })
expect(document.activeElement).toBe(view.body)
if (outcome === 'ended') {
expect(view.queryByRole('button', { name: 'End Terminal 999' })).toBeNull()
} else {
expect(end.hasAttribute('disabled')).toBe(false)
expect(view.getByRole('alert').textContent).toContain('An agent now owns this terminal')
}
}
)
it.each([true, false])(
'preserves focus moved elsewhere before End completes with panel active=%s',
async (active) => {
const view = await livePanel(1)
view.getByRole('button', { name: 'End Terminal 0' }).focus()
await view.user.keyboard('{Enter}')
const elsewhere = view.getByRole('button', { name: 'Elsewhere' })
elsewhere.focus()
view.setActive(active)
await view.finish({ outcome: 'ended' })
expect(document.activeElement).toBe(elsewhere)
}
)
it('hands bulk End focus to the body before the selection becomes unavailable', async () => {
const view = await livePanel(2)
fireEvent.click(view.getByRole('checkbox', { name: 'Select all terminals' }))
const end = view.getByRole('button', { name: 'End selected (2)' })
end.focus()
await view.user.keyboard('{Enter}')
expect(view.kill).toHaveBeenCalledTimes(2)
expect(end.hasAttribute('disabled')).toBe(true)
expect(document.activeElement).toBe(view.body)
await view.finish({ outcome: 'ended' })
expect(view.queryByRole('button', { name: 'End selected (0)' })).toBeNull()
expect(document.activeElement).toBe(view.body)
})
@@ -1,47 +0,0 @@
// @vitest-environment happy-dom
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { ResourceSessionInventoryCache } from './resource-session-inventory-cache'
import { useSyncExternalStore } from 'react'
afterEach(() => {
cleanup()
vi.useRealTimers()
})
it('updates known exits cheaply and commits zero times for 1,000 inactive unknown lifecycle events', async () => {
vi.useFakeTimers()
const list = vi.fn(async () => [
{ id: 'known', incarnationId: 'A', title: 'Shell', cwd: '/', agentOwnership: 'absent' as const }
])
const cache = new ResourceSessionInventoryCache(list, true)
cache.setActive(true)
await cache.refresh()
await vi.advanceTimersByTimeAsync(100)
cache.setActive(false)
let renders = 0
const view = renderHook(() => {
renders++
return useSyncExternalStore(cache.subscribe, cache.getSnapshot)
})
const baseline = view.result.current.sessionInventory
const before = renders
const calls = list.mock.calls.length
for (let index = 0; index < 1_000; index++) {
act(() => {
cache.spawned({ id: `unknown-${index}`, incarnationId: 'A', hostId: 'local' })
cache.exited({ code: 0, id: `unknown-${index}`, incarnationId: 'A', hostId: 'local' })
})
}
await act(async () => {
await vi.advanceTimersByTimeAsync(60_000)
})
expect(renders - before).toBe(0)
expect(view.result.current.sessionInventory).toBe(baseline)
expect(list.mock.calls.length).toBe(calls)
act(() => cache.exited({ code: 0, id: 'known', incarnationId: 'A', hostId: 'local' }))
await act(async () => {
await vi.advanceTimersByTimeAsync(50)
})
expect(view.result.current.sessionInventory.count).toBe(0)
expect(renders - before).toBe(1)
})
@@ -2,27 +2,25 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Badge } from '@/components/ui/badge'
import { Activity, Terminal } from 'lucide-react'
import { useTranslate } from '@/i18n/use-translate'
import { ResourcePtyPanel } from './ResourcePtyPanel'
import { ResourcePtyPanel } from './pty/ResourcePtyPanel'
import React from 'react'
import { Popover, PopoverContent } from '@/components/ui/popover'
import { DaemonActionDialog } from '../shared/useDaemonActions'
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from './status-bar-context-menu-policy'
import { DaemonActionDialog } from '../../shared/useDaemonActions'
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from '../composition/status-bar-context-menu-policy'
import { useResourceUsageStatusController } from './use-resource-usage-status-controller'
import { renderResourceUsageStatusTrigger } from './resource-usage-status-trigger'
import { renderResourceUsageStatusTrigger } from './popover/resource-usage-status-trigger'
import {
renderDaemonUnreachableBanner,
renderResourceUsagePopoverHeader,
renderResourceUsageSummary,
renderSessionsOnlyErrorBanner
} from './resource-usage-popover-summary'
} from './popover/resource-usage-popover-summary'
import {
renderResourceUsagePopoverBody,
renderResourceUsagePopoverFooter
} from './resource-usage-popover-body'
import { renderResourceUsageKillDialog } from './resource-usage-kill-dialog'
import { WorkspaceSpaceCompactPanel } from './WorkspaceSpaceCompactPanel'
export { SessionRow, WorktreeRow } from './resource-usage-session-rows'
} from './popover/resource-usage-popover-body'
import { renderResourceUsageKillDialog } from './popover/resource-usage-kill-dialog'
import { WorkspaceSpaceCompactPanel } from '../workspace-storage/WorkspaceSpaceCompactPanel'
export function ResourceUsageStatusSegment({
iconOnly
@@ -1,6 +1,6 @@
import type { ProviderCheckSummary } from '../../../../shared/github/pull-requests/pull-request-types'
import { getProviderChecksOutcome } from '../../../../shared/source-control/checks/provider-check-summary'
import type { HostedReviewProvider } from '../../../../shared/source-control/hosted-review/hosted-review-contracts'
import type { ProviderCheckSummary } from '../../../../../shared/github/pull-requests/pull-request-types'
import { getProviderChecksOutcome } from '../../../../../shared/source-control/checks/provider-check-summary'
import type { HostedReviewProvider } from '../../../../../shared/source-control/hosted-review/hosted-review-contracts'
import {
localizedHostedReviewCopy,
resolveSupportedHostedReviewCopyProvider
@@ -2,9 +2,9 @@ import {
ptySessionIdentity,
type PtyListedSession,
type PtySessionLifecycle
} from '../../../../shared/terminal/pty/lifecycle/pty-listed-session'
import { isResourceViewInactive } from '../../../../shared/resource-view-admission'
import { isProvenProcessExit } from '../../../../shared/terminal/pty/lifecycle/terminal-exit-cause'
} from '../../../../../../shared/terminal/pty/lifecycle/pty-listed-session'
import { isResourceViewInactive } from '../../../../../../shared/resource-view-admission'
import { isProvenProcessExit } from '../../../../../../shared/terminal/pty/lifecycle/terminal-exit-cause'
import {
type DaemonSessionInventory,
EMPTY_DAEMON_SESSION_INVENTORY,
@@ -1,4 +1,4 @@
import type { PtyListedSession as DaemonSession } from '../../../../shared/terminal/pty/lifecycle/pty-listed-session'
import type { PtyListedSession as DaemonSession } from '../../../../../../shared/terminal/pty/lifecycle/pty-listed-session'
/** Last-known daemon terminal inventory for the Resource Manager badge. */
export type DaemonSessionInventory = {
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
import { subscribeDaemonSessionInventoryInvalidated } from './daemon-session-inventory-invalidation'
import { ResourceSessionInventoryCache } from './resource-session-inventory-cache'
import type { PtyListedSession } from '../../../../shared/terminal/pty/lifecycle/pty-listed-session'
import type { PtyListedSession } from '../../../../../../shared/terminal/pty/lifecycle/pty-listed-session'
export function useResourceSessionInventory(ready: boolean, active = false, localOnly = false) {
const [cache] = useState(
@@ -1,7 +1,7 @@
import type { ProcessMemoryMetric } from '../../../../shared/process-stats-types'
import type { ProcessMemoryMetric } from '../../../../../../shared/process-stats-types'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import { usageTextColorClass } from './usage-roster-formatting'
import { usageTextColorClass } from '../../usage/usage-roster-formatting'
export type ResourceMemoryMetricCopy = {
columnLabel: string
@@ -11,7 +11,7 @@ import {
} from '@/components/ui/dialog'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { UnifiedSessionRow } from './resource-usage-merge-types'
import type { UnifiedSessionRow } from '../workspace-attribution/resource-usage-merge-types'
export function renderResourceUsageKillDialog(
{
@@ -2,8 +2,8 @@ import React, { memo, useMemo } from 'react'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { cn } from '@/lib/class-names'
import { useTranslate } from '@/i18n/use-translate'
import type { AppMemory, UsageValues } from '../../../../shared/process-stats-types'
import type { Metric } from './resource-usage-merge-types'
import type { AppMemory, UsageValues } from '../../../../../../shared/process-stats-types'
import type { Metric } from '../workspace-attribution/resource-usage-merge-types'
export const METRIC_COLUMNS_CLS = 'flex items-center shrink-0 tabular-nums'
export const CPU_COLUMN_CLS = 'w-12 text-right'
@@ -3,8 +3,11 @@ import { ChevronRight } from 'lucide-react'
import { cn } from '@/lib/class-names'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { MemorySnapshot } from '../../../../shared/process-stats-types'
import type { UnifiedProjectGroup, UnifiedSessionRow } from './resource-usage-merge-types'
import type { MemorySnapshot } from '../../../../../../shared/process-stats-types'
import type {
UnifiedProjectGroup,
UnifiedSessionRow
} from '../workspace-attribution/resource-usage-merge-types'
import type { SortOption } from './resource-usage-resource-tree'
import { ResourceTree } from './resource-usage-resource-tree'
import {
@@ -1,7 +1,7 @@
import { defineFixture } from '../../component-fixtures/component-fixture'
import { defineFixture } from '../../../../component-fixtures/component-fixture'
import { renderResourceUsageSummary } from './resource-usage-popover-summary'
import { ResourceUsageStatusSegment } from './ResourceUsageStatusSegment'
import type { MemorySnapshot } from '../../../../shared/process-stats-types'
import { ResourceUsageStatusSegment } from '../ResourceUsageStatusSegment'
import type { MemorySnapshot } from '../../../../../../shared/process-stats-types'
import {
getResourceMemoryMetricCopy,
getResourceCommitMetricCopy
@@ -5,7 +5,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { cn } from '@/lib/class-names'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
import type { DaemonActionsApi } from '../shared/useDaemonActions'
import type { DaemonActionsApi } from '../../../shared/useDaemonActions'
import type {
getResourceCommitMetricCopy,
getResourceMemoryMetricCopy
@@ -1,13 +1,13 @@
import React, { useMemo } from 'react'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { useTranslate } from '@/i18n/use-translate'
import { useWorktreeMap } from '../../store/slices/worktrees/listing/use-worktree-catalog'
import { useWorktreeMap } from '../../../../store/slices/worktrees/listing/use-worktree-catalog'
import type {
Metric,
UnifiedProjectGroup,
UnifiedSessionRow,
UnifiedWorktreeRow
} from './resource-usage-merge-types'
} from '../workspace-attribution/resource-usage-merge-types'
import { MetricPair, ROW_TRAILING_GUTTER_CLS } from './resource-usage-metrics'
import { WorktreeRow } from './resource-usage-session-rows'

Some files were not shown because too many files have changed in this diff Show More