mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 16:02:49 +00:00
Remove unrelated UI merge artifacts
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user