mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
refactor(renderer): organize status bar owners (#42456)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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'
|
||||
|
||||
-190
@@ -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'
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
|
||||
+1
-1
@@ -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()
|
||||
})
|
||||
+2
-2
@@ -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,
|
||||
+3
-3
@@ -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 = {
|
||||
+5
-5
@@ -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.
|
||||
+2
-2
@@ -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'
|
||||
+1
-1
@@ -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,
|
||||
+5
-5
@@ -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 {
|
||||
+7
-4
@@ -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'
|
||||
+2
-2
@@ -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> }
|
||||
|
||||
+4
-4
@@ -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,
|
||||
+2
-2
@@ -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,
|
||||
+3
-5
@@ -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
-1
@@ -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
-1
@@ -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,
|
||||
+2
-2
@@ -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,
|
||||
+2
-2
@@ -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,
|
||||
+5
-5
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import { useAppStore } from '../../store'
|
||||
import { useAppStore } from '../../../store/index'
|
||||
import type { ChatAccountAction } from './ProviderAccountChatButton'
|
||||
import {
|
||||
getClaudeAccountSettings,
|
||||
+5
-4
@@ -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,
|
||||
+1
-1
@@ -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
-2
@@ -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
-1
@@ -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'
|
||||
+2
-2
@@ -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([
|
||||
+3
-3
@@ -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,
|
||||
+5
-5
@@ -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
|
||||
+2
-2
@@ -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. */
|
||||
+12
-9
@@ -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) {
|
||||
+3
-3
@@ -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'
|
||||
|
||||
+2
-2
@@ -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
|
||||
+2
-2
@@ -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,
|
||||
+10
-10
@@ -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,
|
||||
+4
-4
@@ -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)) {
|
||||
+2
-2
@@ -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'
|
||||
|
||||
+6
-6
@@ -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). */
|
||||
+3
-3
@@ -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,
|
||||
+6
-3
@@ -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[] }
|
||||
+2
-2
@@ -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)
|
||||
})
|
||||
-47
@@ -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)
|
||||
})
|
||||
+8
-10
@@ -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
|
||||
+3
-3
@@ -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
|
||||
+3
-3
@@ -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
-1
@@ -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
-1
@@ -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(
|
||||
+2
-2
@@ -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
|
||||
+1
-1
@@ -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
-2
@@ -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'
|
||||
+5
-2
@@ -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 {
|
||||
+3
-3
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+2
-2
@@ -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
Reference in New Issue
Block a user