Remove unrelated UI merge artifacts

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-06-12 11:47:39 -07:00
co-authored by Orca
parent b7d295c690
commit 55066ee185
13 changed files with 429 additions and 610 deletions
@@ -1,78 +0,0 @@
import { Smartphone, Trash2 } from 'lucide-react'
import type { PairedDevice } from './MobileHero'
import { translate } from '@/i18n/i18n'
type HeroPairedProps = {
devices: readonly PairedDevice[]
onPairAnother: () => void
onRevoke: (deviceId: string) => void
revokingDeviceIds: readonly string[]
}
export function HeroPaired({
devices,
onPairAnother,
onRevoke,
revokingDeviceIds
}: HeroPairedProps): React.JSX.Element {
return (
<div>
<div className="mp-eyebrow-row">
<span className="mp-eyebrow">
{translate('auto.components.mobile.MobileHero.5410d55d79', 'Orca Mobile')}
</span>
</div>
<h1 className="mp-h1">
{devices.length === 1
? translate('auto.components.mobile.MobileHero.051978a785', 'Your phone is paired.')
: translate('auto.components.mobile.MobileHero.d0b52871ce', 'Your phones are paired.')}
</h1>
<p className="mp-lead-sm">
{translate(
'auto.components.mobile.MobileHero.266c18c105',
'Open Orca Mobile to pick up where you left off, or pair another device.'
)}
</p>
<ul className="mp-paired-list">
{devices.map((device) => {
const revoking = revokingDeviceIds.includes(device.deviceId)
return (
<li key={device.deviceId} className="mp-paired-row">
<div className="mp-paired-icon">
<Smartphone className="size-4" />
</div>
<div className="mp-paired-main">
<div className="mp-paired-name">{device.name}</div>
<div className="mp-paired-meta">
{translate('auto.components.mobile.MobileHero.94829abdb1', 'Paired')}{' '}
{new Date(device.pairedAt).toLocaleDateString()}
</div>
</div>
<button
type="button"
className="mp-paired-revoke"
onClick={() => onRevoke(device.deviceId)}
disabled={revoking}
aria-label={translate(
'auto.components.mobile.MobileHero.34f878d04f',
'Revoke {{value0}}',
{ value0: device.name }
)}
title={translate('auto.components.mobile.MobileHero.f9cbf4bb53', 'Revoke device')}
>
<Trash2 className="size-3.5" />
</button>
</li>
)
})}
</ul>
<div className="mp-flow-actions">
<button type="button" className="mp-secondary-action" onClick={onPairAnother}>
<Smartphone className="size-3.5" />
{translate('auto.components.mobile.MobileHero.ff48d9d520', 'Pair another device')}
</button>
<span />
</div>
</div>
)
}
@@ -1,92 +0,0 @@
import { Check } from 'lucide-react'
import type { GhosttyImportPreview } from '../../../../shared/types'
import ghosttyIcon from '../../../../../resources/ghostty.svg'
import type { DiscoveryState } from './ThemeStep'
import { translate } from '@/i18n/i18n'
type GhosttyDiscoveryRowProps = {
discovery: DiscoveryState
importing: boolean
disabled: boolean
onImport: (preview: GhosttyImportPreview) => void
}
export function GhosttyDiscoveryRow({
discovery,
importing,
disabled,
onImport
}: GhosttyDiscoveryRowProps): React.JSX.Element | null {
if (discovery.status === 'absent' || discovery.status === 'idle') {
return null
}
if (discovery.status === 'detecting') {
return (
<div className="flex items-center gap-2.5 rounded-lg border border-dashed border-border bg-transparent px-3.5 py-2.5 text-[12px] text-muted-foreground">
<span className="size-1.5 animate-pulse rounded-full bg-muted-foreground/60" />
{translate(
'auto.components.onboarding.ThemeStep.2c3aa538f8',
'Looking for a Ghostty config…'
)}
</div>
)
}
if (discovery.status === 'imported') {
return (
<div className="flex items-center gap-2.5 rounded-lg border border-emerald-500/30 bg-emerald-500/[0.07] px-3.5 py-2.5 text-[12px] text-foreground">
<Check className="size-3.5 text-emerald-600 dark:text-emerald-400" strokeWidth={3} />
<span className="flex-1">
<span className="font-medium">
{translate('auto.components.onboarding.ThemeStep.78b6386140', 'Imported from Ghostty.')}
</span>
{discovery.fields.length > 0 && (
<span className="text-muted-foreground"> {discovery.fields.join(' · ')}</span>
)}
</span>
</div>
)
}
const { preview, fields } = discovery
return (
<div className="flex items-center gap-3 rounded-lg border border-violet-500/30 bg-violet-500/[0.06] px-3.5 py-2.5">
<img src={ghosttyIcon} alt="" className="size-4 shrink-0" />
<div className="min-w-0 flex-1">
<div className="text-[12px] text-foreground">
<span className="font-medium">
{translate(
'auto.components.onboarding.ThemeStep.7ee9234e54',
'Ghostty config detected.'
)}
</span>{' '}
<span className="text-muted-foreground">
{translate('auto.components.onboarding.ThemeStep.248c812283', 'Import')}{' '}
{fields.length > 0
? fields.map((f) => f.toLowerCase()).join(', ')
: translate('auto.components.onboarding.ThemeStep.906c4373fe', 'settings')}
?
</span>
</div>
{preview.configPath && (
<div
className="mt-0.5 truncate font-mono text-[10.5px] text-muted-foreground"
title={preview.configPath}
>
{preview.configPath}
</div>
)}
</div>
<button
className="shrink-0 rounded-md bg-foreground px-3 py-1.5 text-[11.5px] font-semibold text-background hover:bg-foreground/90 disabled:opacity-50"
disabled={importing || disabled}
onClick={() => onImport(preview)}
>
{importing
? translate('auto.components.onboarding.ThemeStep.ad19e5c916', 'Importing…')
: translate('auto.components.onboarding.ThemeStep.248c812283', 'Import')}
</button>
</div>
)
}
@@ -1,36 +0,0 @@
import { LoaderCircle, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
type HostedReviewDeleteWorkspaceButtonProps = {
deleting: boolean
onDelete: () => void
}
export function HostedReviewDeleteWorkspaceButton({
deleting,
onDelete
}: HostedReviewDeleteWorkspaceButtonProps): React.JSX.Element {
return (
<Button
type="button"
variant="secondary"
size="xs"
className="w-full cursor-pointer text-[11px] hover:cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
onClick={onDelete}
disabled={deleting}
>
{deleting ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : (
<Trash2 className="size-3.5" />
)}
{deleting
? translate('auto.components.right.sidebar.HostedReviewActions.eefd50457e', 'Deleting...')
: translate(
'auto.components.right.sidebar.HostedReviewActions.e4aca40024',
'Delete Workspace'
)}
</Button>
)
}
@@ -1,41 +0,0 @@
import { CircleDot, LoaderCircle } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
type HostedReviewReopenButtonProps = {
label: string
reopening: boolean
disabled: boolean
onReopen: () => void
}
export function HostedReviewReopenButton({
label,
reopening,
disabled,
onReopen
}: HostedReviewReopenButtonProps): React.JSX.Element {
return (
<Button
type="button"
variant="outline"
size="xs"
className="w-full cursor-pointer text-[11px] hover:cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
onClick={onReopen}
disabled={disabled}
>
{reopening ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : (
<CircleDot className="size-3.5" />
)}
{reopening
? translate('auto.components.right.sidebar.HostedReviewActions.6645ac7dd1', 'Reopening...')
: translate(
'auto.components.right.sidebar.HostedReviewActions.3ce211ece6',
'Reopen {{value0}}',
{ value0: label }
)}
</Button>
)
}
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { AlertCircle, FileCode2, LoaderCircle, Plus, RefreshCw } from 'lucide-react'
import { toast } from 'sonner'
import { useMountedRef } from '@/hooks/useMountedRef'
import type { Repo } from '../../../../shared/types'
import type { Repo, Worktree } from '../../../../shared/types'
import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id'
import {
canInspectLocalMcpConfigRoot,
inspectMcpConfigContent,
@@ -15,11 +16,6 @@ import { extractIpcErrorMessage } from '../../lib/ipc-error'
import { Button } from '../ui/button'
import { isWindowsUserAgent } from '../terminal-pane/pane-helpers'
import { McpConfigFileRow, type LoadedMcpConfigInspection } from './McpConfigFileRow'
import {
EMPTY_MCP_WORKTREES,
countMcpConfigServers,
selectMcpTargetWorktree
} from './mcp-config-inspection-state'
import { McpMissingConfigList } from './McpMissingConfigList'
import { loadMcpConfigInspections } from './mcp-config-inspection'
import { translate } from '@/i18n/i18n'
@@ -28,15 +24,19 @@ type McpConfigSectionProps = {
repo: Repo
}
const EMPTY_WORKTREES: Worktree[] = []
function countServers(configs: LoadedMcpConfigInspection[]): number {
return configs.reduce((sum, config) => sum + config.servers.length, 0)
}
export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Element {
const openFile = useAppStore((state) => state.openFile)
const setActiveView = useAppStore((state) => state.setActiveView)
const setActiveWorktree = useAppStore((state) => state.setActiveWorktree)
const ensureWorktreeRootGroup = useAppStore((state) => state.ensureWorktreeRootGroup)
const activeWorktreeId = useAppStore((state) => state.activeWorktreeId)
const worktreesForRepo = useAppStore(
(state) => state.worktreesByRepo[repo.id] ?? EMPTY_MCP_WORKTREES
)
const worktreesForRepo = useAppStore((state) => state.worktreesByRepo[repo.id] ?? EMPTY_WORKTREES)
const sshConnectionStatus = useAppStore((state) =>
repo.connectionId ? state.sshConnectionStates.get(repo.connectionId)?.status : null
)
@@ -51,10 +51,21 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
const connectionId = repo.connectionId ?? undefined
const isWindows = isWindowsUserAgent()
const targetWorktree = useMemo(
() => selectMcpTargetWorktree(repo, worktreesForRepo, activeWorktreeId),
[activeWorktreeId, repo, worktreesForRepo]
)
const targetWorktree = useMemo(() => {
if (activeWorktreeId && getRepoIdFromWorktreeId(activeWorktreeId) === repo.id) {
return (
worktreesForRepo.find((worktree) => worktree.id === activeWorktreeId) ?? {
id: activeWorktreeId,
path: repo.path
}
)
}
return (
worktreesForRepo.find((worktree) => worktree.isMainWorktree) ??
worktreesForRepo.find((worktree) => worktree.path === repo.path) ??
worktreesForRepo[0] ?? { id: `${repo.id}::${repo.path}`, path: repo.path }
)
}, [activeWorktreeId, repo.id, repo.path, worktreesForRepo])
const targetWorktreeId = targetWorktree.id
const targetRootPath = targetWorktree.path
const detectedCount = useMemo(() => configs.filter((config) => config.exists).length, [configs])
@@ -85,7 +96,7 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
),
[targetRootPath]
)
const serverCount = useMemo(() => countMcpConfigServers(configs), [configs])
const serverCount = useMemo(() => countServers(configs), [configs])
const canCreateStarter = detectedCount === 0 && !inspectionUnavailable
const loadConfigs = useCallback(async (): Promise<void> => {
@@ -1,34 +0,0 @@
import type { Repo, Worktree } from '../../../../shared/types'
import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id'
import type { LoadedMcpConfigInspection } from './McpConfigFileRow'
export const EMPTY_MCP_WORKTREES: Worktree[] = []
export function isMissingMcpConfigFileError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error)
return /ENOENT|no such file|not found/i.test(message)
}
export function countMcpConfigServers(configs: LoadedMcpConfigInspection[]): number {
return configs.reduce((sum, config) => sum + config.servers.length, 0)
}
export function selectMcpTargetWorktree(
repo: Repo,
worktreesForRepo: readonly Worktree[],
activeWorktreeId: string | null
): Pick<Worktree, 'id' | 'path'> {
if (activeWorktreeId && getRepoIdFromWorktreeId(activeWorktreeId) === repo.id) {
return (
worktreesForRepo.find((worktree) => worktree.id === activeWorktreeId) ?? {
id: activeWorktreeId,
path: repo.path
}
)
}
return (
worktreesForRepo.find((worktree) => worktree.isMainWorktree) ??
worktreesForRepo.find((worktree) => worktree.path === repo.path) ??
worktreesForRepo[0] ?? { id: `${repo.id}::${repo.path}`, path: repo.path }
)
}
@@ -5,18 +5,29 @@ import {
DatabaseZap,
FolderKanban,
Gauge,
RefreshCw,
SlidersHorizontal,
Sparkles,
Waypoints
} from 'lucide-react'
import type { ClaudeUsageRange, ClaudeUsageScope } from '../../../../shared/claude-usage-types'
import { useAppStore } from '../../store'
import { Button } from '../ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '../ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { ClaudeUsageDetails } from './ClaudeUsageDetails'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { ShareUsageButton } from './ShareUsageButton'
import { StatCard } from './StatCard'
import { UsagePaneFilterControls } from './UsagePaneFilterControls'
import { UsageTrackingDisabledCard } from './UsageTrackingDisabledCard'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-display-formatting'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-formatters'
import { translate } from '@/i18n/i18n'
const RANGE_OPTIONS: ClaudeUsageRange[] = ['7d', '30d', '90d', 'all']
@@ -76,21 +87,37 @@ export function ClaudeUsagePane(): React.JSX.Element {
if (!scanState?.enabled) {
return (
<UsageTrackingDisabledCard
title={translate(
'auto.components.stats.ClaudeUsagePane.6afacbee37',
'Claude Usage Tracking'
)}
description={translate(
'auto.components.stats.ClaudeUsagePane.0cb1a36d7d',
'Reads local Claude usage logs to show token, model, and session stats.'
)}
enableLabel={translate(
'auto.components.stats.ClaudeUsagePane.424cd50412',
'Enable Claude usage analytics'
)}
onEnable={() => handleSetEnabled(true)}
/>
<div className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold text-foreground">
{translate(
'auto.components.stats.ClaudeUsagePane.6afacbee37',
'Claude Usage Tracking'
)}
</h3>
<p className="text-sm text-muted-foreground">
{translate(
'auto.components.stats.ClaudeUsagePane.0cb1a36d7d',
'Reads local Claude usage logs to show token, model, and session stats.'
)}
</p>
</div>
<button
type="button"
role="switch"
aria-checked={false}
aria-label={translate(
'auto.components.stats.ClaudeUsagePane.424cd50412',
'Enable Claude usage analytics'
)}
onClick={() => handleSetEnabled(true)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-muted-foreground/30 transition-colors"
>
<span className="pointer-events-none block size-3.5 translate-x-0.5 rounded-full bg-background shadow-sm transition-transform" />
</button>
</div>
</div>
)
}
@@ -122,30 +149,92 @@ export function ClaudeUsagePane(): React.JSX.Element {
{summary && daily.length > 0 && (
<ShareUsageButton provider="claude" summary={summary} daily={daily} range={range} />
)}
<UsagePaneFilterControls
scope={scope}
range={range}
scopeOptions={SCOPE_OPTIONS}
rangeOptions={RANGE_OPTIONS}
rangeLabels={RANGE_LABELS}
isScanning={scanState.isScanning}
optionsLabel={translate(
'auto.components.stats.ClaudeUsagePane.e9bf9fce0e',
'Claude usage options'
)}
filtersLabel={translate('auto.components.stats.ClaudeUsagePane.dd29209b21', 'Filters')}
scopeLabel={translate('auto.components.stats.ClaudeUsagePane.f61cffb9c8', 'Scope')}
rangeLabel={translate('auto.components.stats.ClaudeUsagePane.505be9aac4', 'Range')}
refreshLabel={translate('auto.components.stats.ClaudeUsagePane.8d18bbb771', 'Refresh')}
enableLabel={translate(
<DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
aria-label={translate(
'auto.components.stats.ClaudeUsagePane.e9bf9fce0e',
'Claude usage options'
)}
>
<SlidersHorizontal className="size-3.5" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.stats.ClaudeUsagePane.dd29209b21', 'Filters')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuLabel>
{translate('auto.components.stats.ClaudeUsagePane.f61cffb9c8', 'Scope')}
</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={scope}
onValueChange={(value) => void setClaudeUsageScope(value as ClaudeUsageScope)}
>
{SCOPE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value}>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuLabel>
{translate('auto.components.stats.ClaudeUsagePane.505be9aac4', 'Range')}
</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={range}
onValueChange={(value) => void setClaudeUsageRange(value as ClaudeUsageRange)}
>
{RANGE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option} value={option}>
{RANGE_LABELS[option]}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
onClick={() => void refreshClaudeUsage()}
disabled={scanState.isScanning}
aria-label={translate(
'auto.components.stats.ClaudeUsagePane.c5b9b344d0',
'Refresh Claude usage'
)}
>
<RefreshCw className={`size-3.5 ${scanState.isScanning ? 'animate-spin' : ''}`} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.stats.ClaudeUsagePane.8d18bbb771', 'Refresh')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<button
type="button"
role="switch"
aria-checked={true}
aria-label={translate(
'auto.components.stats.ClaudeUsagePane.424cd50412',
'Enable Claude usage analytics'
)}
onScopeChange={(value) => void setClaudeUsageScope(value)}
onRangeChange={(value) => void setClaudeUsageRange(value)}
onRefresh={() => void refreshClaudeUsage()}
onDisable={() => handleSetEnabled(false)}
/>
onClick={() => handleSetEnabled(false)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
>
<span className="pointer-events-none block size-3.5 translate-x-4 rounded-full bg-background shadow-sm transition-transform" />
</button>
</div>
</div>
@@ -1,14 +1,32 @@
import { useEffect } from 'react'
import { Activity, Brain, Coins, DatabaseZap, FolderKanban, Sparkles } from 'lucide-react'
import {
Activity,
Brain,
Coins,
DatabaseZap,
FolderKanban,
RefreshCw,
SlidersHorizontal,
Sparkles
} from 'lucide-react'
import type { CodexUsageRange, CodexUsageScope } from '../../../../shared/codex-usage-types'
import { useAppStore } from '../../store'
import { Button } from '../ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '../ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { CodexUsageDetails } from './CodexUsageDetails'
import { ShareUsageButton } from './ShareUsageButton'
import { StatCard } from './StatCard'
import { UsagePaneFilterControls } from './UsagePaneFilterControls'
import { UsageTrackingDisabledCard } from './UsageTrackingDisabledCard'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-display-formatting'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-formatters'
import { translate } from '@/i18n/i18n'
const RANGE_OPTIONS: CodexUsageRange[] = ['7d', '30d', '90d', 'all']
@@ -68,18 +86,34 @@ export function CodexUsagePane(): React.JSX.Element {
if (!scanState?.enabled) {
return (
<UsageTrackingDisabledCard
title={translate('auto.components.stats.CodexUsagePane.408210470c', 'Codex Usage Tracking')}
description={translate(
'auto.components.stats.CodexUsagePane.13badcd8f2',
'Reads local Codex usage logs to show token, model, and session stats.'
)}
enableLabel={translate(
'auto.components.stats.CodexUsagePane.f7c1affbd5',
'Enable Codex usage analytics'
)}
onEnable={() => handleSetEnabled(true)}
/>
<div className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.CodexUsagePane.408210470c', 'Codex Usage Tracking')}
</h3>
<p className="text-sm text-muted-foreground">
{translate(
'auto.components.stats.CodexUsagePane.13badcd8f2',
'Reads local Codex usage logs to show token, model, and session stats.'
)}
</p>
</div>
<button
type="button"
role="switch"
aria-checked={false}
aria-label={translate(
'auto.components.stats.CodexUsagePane.f7c1affbd5',
'Enable Codex usage analytics'
)}
onClick={() => handleSetEnabled(true)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-muted-foreground/30 transition-colors"
>
<span className="pointer-events-none block size-3.5 translate-x-0.5 rounded-full bg-background shadow-sm transition-transform" />
</button>
</div>
</div>
)
}
@@ -117,30 +151,92 @@ export function CodexUsagePane(): React.JSX.Element {
{summary && daily.length > 0 && (
<ShareUsageButton provider="codex" summary={summary} daily={daily} range={range} />
)}
<UsagePaneFilterControls
scope={scope}
range={range}
scopeOptions={SCOPE_OPTIONS}
rangeOptions={RANGE_OPTIONS}
rangeLabels={RANGE_LABELS}
isScanning={scanState.isScanning}
optionsLabel={translate(
'auto.components.stats.CodexUsagePane.70b5b8581f',
'Codex usage options'
)}
filtersLabel={translate('auto.components.stats.CodexUsagePane.1af1a39b2f', 'Filters')}
scopeLabel={translate('auto.components.stats.CodexUsagePane.6d68e8399a', 'Scope')}
rangeLabel={translate('auto.components.stats.CodexUsagePane.89162e019b', 'Range')}
refreshLabel={translate('auto.components.stats.CodexUsagePane.3022cda443', 'Refresh')}
enableLabel={translate(
<DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
aria-label={translate(
'auto.components.stats.CodexUsagePane.70b5b8581f',
'Codex usage options'
)}
>
<SlidersHorizontal className="size-3.5" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.stats.CodexUsagePane.1af1a39b2f', 'Filters')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuLabel>
{translate('auto.components.stats.CodexUsagePane.6d68e8399a', 'Scope')}
</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={scope}
onValueChange={(value) => void setCodexUsageScope(value as CodexUsageScope)}
>
{SCOPE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value}>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuLabel>
{translate('auto.components.stats.CodexUsagePane.89162e019b', 'Range')}
</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={range}
onValueChange={(value) => void setCodexUsageRange(value as CodexUsageRange)}
>
{RANGE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option} value={option}>
{RANGE_LABELS[option]}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
onClick={() => void refreshCodexUsage()}
disabled={scanState.isScanning}
aria-label={translate(
'auto.components.stats.CodexUsagePane.ec4d270e2c',
'Refresh Codex usage'
)}
>
<RefreshCw className={`size-3.5 ${scanState.isScanning ? 'animate-spin' : ''}`} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.stats.CodexUsagePane.3022cda443', 'Refresh')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<button
type="button"
role="switch"
aria-checked={true}
aria-label={translate(
'auto.components.stats.CodexUsagePane.f7c1affbd5',
'Enable Codex usage analytics'
)}
onScopeChange={(value) => void setCodexUsageScope(value)}
onRangeChange={(value) => void setCodexUsageRange(value)}
onRefresh={() => void refreshCodexUsage()}
onDisable={() => handleSetEnabled(false)}
/>
onClick={() => handleSetEnabled(false)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
>
<span className="pointer-events-none block size-3.5 translate-x-4 rounded-full bg-background shadow-sm transition-transform" />
</button>
</div>
</div>
@@ -1,16 +1,34 @@
import { useEffect } from 'react'
import { Activity, Brain, Coins, DatabaseZap, FolderKanban, Sparkles } from 'lucide-react'
import {
Activity,
Brain,
Coins,
DatabaseZap,
FolderKanban,
RefreshCw,
SlidersHorizontal,
Sparkles
} from 'lucide-react'
import type {
OpenCodeUsageRange,
OpenCodeUsageScope
} from '../../../../shared/opencode-usage-types'
import { useAppStore } from '../../store'
import { Button } from '../ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '../ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { OpenCodeUsageDetails } from './OpenCodeUsageDetails'
import { StatCard } from './StatCard'
import { UsagePaneFilterControls } from './UsagePaneFilterControls'
import { UsageTrackingDisabledCard } from './UsageTrackingDisabledCard'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-display-formatting'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-formatters'
import { translate } from '@/i18n/i18n'
const RANGE_OPTIONS: OpenCodeUsageRange[] = ['7d', '30d', '90d', 'all']
@@ -73,21 +91,37 @@ export function OpenCodeUsagePane(): React.JSX.Element {
if (!scanState?.enabled) {
return (
<UsageTrackingDisabledCard
title={translate(
'auto.components.stats.OpenCodeUsagePane.bea80ceae0',
'OpenCode Usage Tracking'
)}
description={translate(
'auto.components.stats.OpenCodeUsagePane.b8b3522436',
'Reads local OpenCode usage logs to show token, model, and session stats.'
)}
enableLabel={translate(
'auto.components.stats.OpenCodeUsagePane.f04131b3be',
'Enable OpenCode usage analytics'
)}
onEnable={() => handleSetEnabled(true)}
/>
<div className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold text-foreground">
{translate(
'auto.components.stats.OpenCodeUsagePane.bea80ceae0',
'OpenCode Usage Tracking'
)}
</h3>
<p className="text-sm text-muted-foreground">
{translate(
'auto.components.stats.OpenCodeUsagePane.b8b3522436',
'Reads local OpenCode usage logs to show token, model, and session stats.'
)}
</p>
</div>
<button
type="button"
role="switch"
aria-checked={false}
aria-label={translate(
'auto.components.stats.OpenCodeUsagePane.f04131b3be',
'Enable OpenCode usage analytics'
)}
onClick={() => handleSetEnabled(true)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-muted-foreground/30 transition-colors"
>
<span className="pointer-events-none block size-3.5 translate-x-0.5 rounded-full bg-background shadow-sm transition-transform" />
</button>
</div>
</div>
)
}
@@ -127,30 +161,94 @@ export function OpenCodeUsagePane(): React.JSX.Element {
: ''}
</p>
</div>
<UsagePaneFilterControls
scope={scope}
range={range}
scopeOptions={SCOPE_OPTIONS}
rangeOptions={RANGE_OPTIONS}
rangeLabels={RANGE_LABELS}
isScanning={scanState.isScanning}
optionsLabel={translate(
'auto.components.stats.OpenCodeUsagePane.230d6de108',
'OpenCode usage options'
)}
filtersLabel={translate('auto.components.stats.OpenCodeUsagePane.01583b30aa', 'Filters')}
scopeLabel={translate('auto.components.stats.OpenCodeUsagePane.40d283c837', 'Scope')}
rangeLabel={translate('auto.components.stats.OpenCodeUsagePane.b5ed5c9fd0', 'Range')}
refreshLabel={translate('auto.components.stats.OpenCodeUsagePane.603cd138dc', 'Refresh')}
enableLabel={translate(
'auto.components.stats.OpenCodeUsagePane.f04131b3be',
'Enable OpenCode usage analytics'
)}
onScopeChange={(value) => void setOpenCodeUsageScope(value)}
onRangeChange={(value) => void setOpenCodeUsageRange(value)}
onRefresh={() => void refreshOpenCodeUsage()}
onDisable={() => handleSetEnabled(false)}
/>
<div className="flex shrink-0 items-center gap-2 self-start">
<DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
aria-label={translate(
'auto.components.stats.OpenCodeUsagePane.230d6de108',
'OpenCode usage options'
)}
>
<SlidersHorizontal className="size-3.5" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.stats.OpenCodeUsagePane.01583b30aa', 'Filters')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuLabel>
{translate('auto.components.stats.OpenCodeUsagePane.40d283c837', 'Scope')}
</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={scope}
onValueChange={(value) => void setOpenCodeUsageScope(value as OpenCodeUsageScope)}
>
{SCOPE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value}>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuLabel>
{translate('auto.components.stats.OpenCodeUsagePane.b5ed5c9fd0', 'Range')}
</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={range}
onValueChange={(value) => void setOpenCodeUsageRange(value as OpenCodeUsageRange)}
>
{RANGE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option} value={option}>
{RANGE_LABELS[option]}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
onClick={() => void refreshOpenCodeUsage()}
disabled={scanState.isScanning}
aria-label={translate(
'auto.components.stats.OpenCodeUsagePane.bed558df0b',
'Refresh OpenCode usage'
)}
>
<RefreshCw className={`size-3.5 ${scanState.isScanning ? 'animate-spin' : ''}`} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate('auto.components.stats.OpenCodeUsagePane.603cd138dc', 'Refresh')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<button
type="button"
role="switch"
aria-checked={true}
aria-label={translate(
'auto.components.stats.OpenCodeUsagePane.f04131b3be',
'Enable OpenCode usage analytics'
)}
onClick={() => handleSetEnabled(false)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
>
<span className="pointer-events-none block size-3.5 translate-x-4 rounded-full bg-background shadow-sm transition-transform" />
</button>
</div>
</div>
<div className="flex items-center justify-between gap-3">
@@ -1,124 +0,0 @@
import { RefreshCw, SlidersHorizontal } from 'lucide-react'
import { Button } from '../ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '../ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
type UsagePaneFilterControlsProps<Scope extends string, Range extends string> = {
scope: Scope
range: Range
scopeOptions: { value: Scope; label: string }[]
rangeOptions: Range[]
rangeLabels: Record<Range, string>
isScanning: boolean
optionsLabel: string
filtersLabel: string
scopeLabel: string
rangeLabel: string
refreshLabel: string
enableLabel: string
onScopeChange: (value: Scope) => void
onRangeChange: (value: Range) => void
onRefresh: () => void
onDisable: () => void
}
export function UsagePaneFilterControls<Scope extends string, Range extends string>({
scope,
range,
scopeOptions,
rangeOptions,
rangeLabels,
isScanning,
optionsLabel,
filtersLabel,
scopeLabel,
rangeLabel,
refreshLabel,
enableLabel,
onScopeChange,
onRangeChange,
onRefresh,
onDisable
}: UsagePaneFilterControlsProps<Scope, Range>): React.JSX.Element {
return (
<div className="flex shrink-0 items-center gap-2 self-start">
<DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon-xs" aria-label={optionsLabel}>
<SlidersHorizontal className="size-3.5" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{filtersLabel}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align="end" className="w-60">
<DropdownMenuLabel>{scopeLabel}</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={scope}
onValueChange={(value) => onScopeChange(value as Scope)}
>
{scopeOptions.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value}>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuLabel>{rangeLabel}</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={range}
onValueChange={(value) => onRangeChange(value as Range)}
>
{rangeOptions.map((option) => (
<DropdownMenuRadioItem key={option} value={option}>
{rangeLabels[option]}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
onClick={onRefresh}
disabled={isScanning}
aria-label={refreshLabel}
>
<RefreshCw className={`size-3.5 ${isScanning ? 'animate-spin' : ''}`} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{refreshLabel}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<button
type="button"
role="switch"
aria-checked={true}
aria-label={enableLabel}
onClick={onDisable}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
>
<span className="pointer-events-none block size-3.5 translate-x-4 rounded-full bg-background shadow-sm transition-transform" />
</button>
</div>
)
}
@@ -1,34 +0,0 @@
type UsageTrackingDisabledCardProps = {
title: string
description: string
enableLabel: string
onEnable: () => void
}
export function UsageTrackingDisabledCard({
title,
description,
enableLabel,
onEnable
}: UsageTrackingDisabledCardProps): React.JSX.Element {
return (
<div className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-2">
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
<button
type="button"
role="switch"
aria-checked={false}
aria-label={enableLabel}
onClick={onEnable}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-muted-foreground/30 transition-colors"
>
<span className="pointer-events-none block size-3.5 translate-x-0.5 rounded-full bg-background shadow-sm transition-transform" />
</button>
</div>
</div>
)
}
@@ -1,36 +0,0 @@
export function formatTokens(value: number): string {
if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(1)}M`
}
if (value >= 1_000) {
return `${(value / 1_000).toFixed(1)}k`
}
return value.toLocaleString()
}
export function formatCost(value: number | null): string {
if (value === null) {
return 'n/a'
}
return value < 0.01 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`
}
export function formatUpdatedAt(timestamp: number | null): string {
if (!timestamp) {
return 'Not scanned yet'
}
return `Updated ${new Date(timestamp).toLocaleString()}`
}
export function formatSessionTime(timestamp: string): string {
const parsed = new Date(timestamp)
if (Number.isNaN(parsed.getTime())) {
return timestamp
}
return parsed.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})
}
@@ -6,14 +6,14 @@ type FolderWorkspacePathStatusCacheClockEntry = {
}
export function useFolderWorkspacePathStatusCacheExpiryTick(
entries: Record<string, FolderWorkspacePathStatusCacheClockEntry> | null | undefined
entries: Record<string, FolderWorkspacePathStatusCacheClockEntry>
): number {
const [tick, setTick] = useState(0)
useEffect(() => {
const now = Date.now()
let nextDelayMs = Number.POSITIVE_INFINITY
for (const entry of Object.values(entries ?? {})) {
for (const entry of Object.values(entries)) {
const delayMs = entry.checkedAt + FOLDER_WORKSPACE_PATH_STATUS_TTL_MS - now
if (delayMs > 0) {
nextDelayMs = Math.min(nextDelayMs, delayMs)