Merge remote-tracking branch 'origin/main' into brennanb2025/linear-cli-v2

# Conflicts:
#	src/renderer/src/components/mobile/MobileHero.tsx
#	src/renderer/src/components/onboarding/ThemeStep.tsx
#	src/renderer/src/components/right-sidebar/HostedReviewActions.tsx
#	src/renderer/src/components/settings/McpConfigSection.tsx
#	src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts
#	src/renderer/src/components/stats/ClaudeUsagePane.tsx
#	src/renderer/src/components/stats/CodexUsagePane.tsx
#	src/renderer/src/components/stats/OpenCodeUsagePane.tsx
This commit is contained in:
brennanb2025
2026-06-11 11:50:16 -07:00
33 changed files with 1141 additions and 612 deletions
+71 -1
View File
@@ -153,5 +153,75 @@ export const JA_VALUE_OVERRIDES = {
'Enter a commit message to commit': 'コミットメッセージを入力してコミット',
'Choose parent folder...': '親フォルダーを選択...',
'Check for stuck work, stale generated files, failing validation, and anything that needs human attention. Report only actionable issues.':
'スタックした作業、古い生成ファイル、検証の失敗、および人間の対応が必要なものがないか確認してください。対応が必要なイシューのみを報告してください。'
'スタックした作業、古い生成ファイル、検証の失敗、および人間の対応が必要なものがないか確認してください。対応が必要なイシューのみを報告してください。',
// Round 6: homograph/term mistranslations surfaced by a full ja.json audit.
// Why: machine translation rendered UI terms as unrelated everyday words.
Smart: 'スマート',
Jobs: 'ジョブ',
Bold: '太字',
Strike: '取り消し線',
thread: 'スレッド',
Lead: 'リード',
Assignees: '担当者',
'Assignees:': '担当者:',
Origin: 'オリジン',
Force: '強制',
Kind: '種類',
Address: 'アドレス',
Mouse: 'マウス',
Home: 'ホーム',
Move: '移動',
Change: '変更',
Conflicts: '競合',
conflict: '競合',
'unresolved conflict': '未解決の競合',
'Next match': '次の一致',
'Previous match': '前の一致',
'No matches': '一致なし',
'New Issue': '新規イシュー',
'Issue title': 'イシューのタイトル',
'Sub-issue title': 'サブイシューのタイトル',
Merged: 'マージ済み',
Behind: '遅れ',
'Head branch': 'ヘッドブランチ',
Import: 'インポート',
'Import…': 'インポート…',
'Import...': 'インポート...',
'Re-import': '再インポート',
Local: 'ローカル',
local: 'ローカル',
'Scanning...': 'スキャン中...',
Sparse: 'スパース',
sparse: 'スパース',
connection: '接続',
'Est. cost': '推定コスト',
'Est. API-equivalent cost': 'API 相当の推定コスト',
'Est. spend': '推定費用',
Spend: '費用',
'new markdown': '新規 markdown',
working: '実行中',
'Working…': '処理中…',
On: 'オン',
Hold: 'ホールド',
'Keep alive until reset': 'リセットされるまで維持',
Forward: '進む',
Fresh: '新規',
Gone: '削除済み',
Ding: 'ディン',
'Take back': '操作を取り戻す',
'Mobile driving': 'モバイルで操作中',
'Mobile is driving this browser': 'モバイルがこのブラウザを操作しています',
'Wants to run': '実行をリクエスト中',
'No states found': 'ステータスが見つかりません',
'Loading states': 'ステータスを読み込み中',
vibrancy: '透過効果',
ligature: '合字',
Zinc: 'ジンク',
Rose: 'ローズ',
import: 'インポート',
mouse: 'マウス',
'Open job in GitLab': 'GitLab でジョブを開く',
'Showing first 100 jobs': '最初の 100 件のジョブを表示しています',
'Imported from Ghostty.': 'Ghostty からインポートしました。',
'Claude Accounts': 'Claude アカウント'
}
+2
View File
@@ -590,5 +590,7 @@ export const LOCALE_KEY_OVERRIDES = {
zh: '已添加评审评论。',
ja: 'レビューコメントを追加しました。'
},
// Port forwarding "Forward" is 転送, not the browser-navigation 進む.
'auto.components.right.sidebar.PortsPanel.c9d106547a': { ja: '転送' },
...KO_KEY_OVERRIDES
}
+69 -1
View File
@@ -82,7 +82,75 @@ export const NEVER_TRANSLATE_VALUES = new Set([
'Nautilus',
'GitHub',
'no_proxy',
'Beta'
'Beta',
// Round 6: product/tool names, language names, and code tokens that machine
// translation wrongly localized (e.g. tailscale→尾鱗, Swift→迅速, yarn→糸).
'Tailscale',
'tailscale',
'Ghostty',
'ghostty',
'pwsh',
'yarn',
'Kagi',
'kagi',
'Bitbucket',
'bitbucket',
'GNOME',
'gnome',
'iCloud',
'icloud',
'ripgrep',
'PowerShell',
'powershell',
'Mermaid',
'mermaid',
'Swift',
'swift',
'Rust',
'rust',
'Java',
'java',
'Go',
'Python',
'python',
'Kotlin',
'kotlin',
'Ruby',
'ruby',
'Bash',
'bash',
'GraphQL',
'graphql',
'ide',
'IDE',
'ui',
'UI',
'otlp',
'OTLP',
'calt',
'ai',
'AI',
'ci',
'REST',
'rest',
'yaml',
'yml',
'true',
'false',
'/home/user',
'/home/user/project',
'/path/to/destination',
'.orca/issue-command',
'PLAN.md',
'feat/mobile-page',
'sk-...',
'main',
'master',
'HEAD',
'lint',
'MD',
'/home/user/projects',
'Claude Code'
])
export const BRAND_MISTRANSLATIONS = {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "orca",
"version": "1.4.61-rc.2",
"version": "1.4.62",
"description": "Next-gen IDE for parallel agentic development",
"homepage": "https://github.com/stablyai/orca",
"author": "stablyai",
@@ -0,0 +1,16 @@
// Why: these are exact filled brand paths, not generic line approximations.
export function IosBrandIcon(): React.JSX.Element {
return (
<svg className="mp-platform-brand-icon" viewBox="0 0 24 24" aria-hidden>
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" />
</svg>
)
}
export function AndroidLogo(): React.JSX.Element {
return (
<svg className="mp-platform-brand-icon" viewBox="0 0 24 24" aria-hidden>
<path d="M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z" />
</svg>
)
}
@@ -2,6 +2,8 @@ import { ArrowLeft, ArrowRight, Copy, RefreshCw } from 'lucide-react'
import { cn } from '../../lib/utils'
import type { MobileNetworkInterface } from '../settings/mobile-network-interface-selection'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { AndroidLogo, IosBrandIcon } from './MobileBrandIcons'
export { HeroIntro } from './MobileHeroIntro'
export { HeroPaired } from './MobileHeroPaired'
import { translate } from '@/i18n/i18n'
@@ -27,59 +29,6 @@ function getDeviceLabel(): string {
return 'computer'
}
export function HeroIntro({ onStart }: { onStart: () => void }): React.JSX.Element {
return (
<div className="mp-intro-shell">
<div className="mp-eyebrow-row">
<span className="mp-eyebrow">
{translate('auto.components.mobile.MobileHero.5410d55d79', 'Orca Mobile')}
</span>
</div>
<h1 className="mp-h1">
{translate(
'auto.components.mobile.MobileHero.cd4e5e816f',
'Your workspaces, in your pocket.'
)}
</h1>
<p className="mp-lead">
{translate(
'auto.components.mobile.MobileHero.b4ccce5cb7',
"Control Orca from your phone. Check on agents, review changes, and kick off tasks while you're away from your desk."
)}
</p>
<div
className="mp-platform-badges"
aria-label={translate(
'auto.components.mobile.MobileHero.ec0607bf66',
'Supported mobile platforms'
)}
>
<span className="mp-platform-label">
{translate('auto.components.mobile.MobileHero.da1d5e5ed0', 'Available on')}
</span>
<span className="mp-platform-badge">
<IosBrandIcon />
{translate('auto.components.mobile.MobileHero.711e6f4b47', 'iOS')}
</span>
<span className="mp-platform-badge">
<AndroidLogo />
{translate('auto.components.mobile.MobileHero.ac1eb64952', 'Android')}
</span>
</div>
<div className="mp-cta-row">
<button
type="button"
className="mp-primary-action mp-flow-primary-action"
onClick={onStart}
>
{translate('auto.components.mobile.MobileHero.10d27b4cba', 'Get started')}
<ArrowRight className="size-3.5" />
</button>
</div>
</div>
)
}
type HeroFlowProps = {
stepIdx: StepIndex
platform: Platform
@@ -350,20 +299,3 @@ export function HeroFlow({
</div>
)
}
// Why: these are exact filled brand paths, not generic line approximations.
function IosBrandIcon(): React.JSX.Element {
return (
<svg className="mp-platform-brand-icon" viewBox="0 0 24 24" aria-hidden>
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" />
</svg>
)
}
function AndroidLogo(): React.JSX.Element {
return (
<svg className="mp-platform-brand-icon" viewBox="0 0 24 24" aria-hidden>
<path d="M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z" />
</svg>
)
}
@@ -0,0 +1,56 @@
import { ArrowRight } from 'lucide-react'
import { AndroidLogo, IosBrandIcon } from './MobileBrandIcons'
import { translate } from '@/i18n/i18n'
export function HeroIntro({ onStart }: { onStart: () => void }): React.JSX.Element {
return (
<div className="mp-intro-shell">
<div className="mp-eyebrow-row">
<span className="mp-eyebrow">
{translate('auto.components.mobile.MobileHero.5410d55d79', 'Orca Mobile')}
</span>
</div>
<h1 className="mp-h1">
{translate(
'auto.components.mobile.MobileHero.cd4e5e816f',
'Your workspaces, in your pocket.'
)}
</h1>
<p className="mp-lead">
{translate(
'auto.components.mobile.MobileHero.b4ccce5cb7',
"Control Orca from your phone. Check on agents, review changes, and kick off tasks while you're away from your desk."
)}
</p>
<div
className="mp-platform-badges"
aria-label={translate(
'auto.components.mobile.MobileHero.ec0607bf66',
'Supported mobile platforms'
)}
>
<span className="mp-platform-label">
{translate('auto.components.mobile.MobileHero.da1d5e5ed0', 'Available on')}
</span>
<span className="mp-platform-badge">
<IosBrandIcon />
{translate('auto.components.mobile.MobileHero.711e6f4b47', 'iOS')}
</span>
<span className="mp-platform-badge">
<AndroidLogo />
{translate('auto.components.mobile.MobileHero.ac1eb64952', 'Android')}
</span>
</div>
<div className="mp-cta-row">
<button
type="button"
className="mp-primary-action mp-flow-primary-action"
onClick={onStart}
>
{translate('auto.components.mobile.MobileHero.10d27b4cba', 'Get started')}
<ArrowRight className="size-3.5" />
</button>
</div>
</div>
)
}
@@ -0,0 +1,93 @@
import { Check } from 'lucide-react'
import type { GhosttyImportPreview } from '../../../../shared/types'
import ghosttyIcon from '../../../../../resources/ghostty.svg'
import { translate } from '@/i18n/i18n'
import type { DiscoveryState } from './ThemeStep'
export function GhosttyDiscoveryRow({
discovery,
importing,
disabled,
onImport
}: {
discovery: DiscoveryState
importing: boolean
disabled: boolean
onImport: (preview: GhosttyImportPreview) => void
}) {
// Why: 'idle' is the pre-effect state that persists on non-Mac (the
// discovery effect short-circuits there), so render nothing instead of
// showing the dashed-border "Looking for a Ghostty config..." placeholder.
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>
)
}
@@ -4,12 +4,12 @@ import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import { track } from '@/lib/telemetry'
import { useMountedRef } from '@/hooks/useMountedRef'
import { GhosttyDiscoveryRow } from './GhosttyDiscoveryRow'
import type {
DiscoveryStatusEmitted,
GhosttyImportPreview,
GlobalSettings
} from '../../../../shared/types'
import { GhosttyDiscoveryRow } from './ThemeStepGhosttyDiscoveryRow'
import { translate } from '@/i18n/i18n'
type ThemeStepProps = {
@@ -19,9 +19,12 @@ import type { PRInfo, Repo, Worktree } from '../../../../shared/types'
import type { GitHubPRMergeMethod } from '../../../../shared/types'
import { resolveGitHubPRMergeMethods } from '../../../../shared/github-pr-merge-methods'
import { runWorktreeDelete } from '../sidebar/delete-worktree-flow'
import { HostedReviewDeleteWorkspaceButton } from './HostedReviewDeleteWorkspaceButton'
import { HostedReviewReopenButton } from './HostedReviewReopenButton'
import { presentGitLabMRMergeState } from './gitlab-mr-merge-state'
import {
ClosedReviewActions,
HostedReviewActionError,
MergedReviewActions
} from './HostedReviewStateActions'
import { translate } from '@/i18n/i18n'
type HostedReviewActionInfo = Pick<
@@ -371,30 +374,27 @@ export default function HostedReviewActions({
</DropdownMenu>
</div>
</TooltipProvider>
{actionError && <div className="text-[10px] text-rose-500 break-words">{actionError}</div>}
<HostedReviewActionError message={actionError} />
</div>
)
}
if (review.state === 'closed') {
return (
<div className="space-y-1.5">
<HostedReviewReopenButton
label={shortLabel}
reopening={stateUpdating === 'open'}
disabled={isUpdatingReviewState}
onReopen={() => void handleReopenReview()}
/>
{actionError && <div className="text-[10px] text-rose-500 break-words">{actionError}</div>}
</div>
<ClosedReviewActions
shortLabel={shortLabel}
stateUpdating={stateUpdating}
actionError={actionError}
onReopenReview={() => void handleReopenReview()}
/>
)
}
if (review.state === 'merged') {
return (
<HostedReviewDeleteWorkspaceButton
deleting={isDeletingWorktree}
onDelete={handleDeleteWorktree}
<MergedReviewActions
isDeletingWorktree={isDeletingWorktree}
onDeleteWorktree={handleDeleteWorktree}
/>
)
}
@@ -0,0 +1,84 @@
import { CircleDot, LoaderCircle, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
export function HostedReviewActionError({
message
}: {
message: string | null
}): React.JSX.Element | null {
return message ? <div className="text-[10px] text-rose-500 break-words">{message}</div> : null
}
export function ClosedReviewActions({
shortLabel,
stateUpdating,
actionError,
onReopenReview
}: {
shortLabel: string
stateUpdating: 'open' | 'closed' | null
actionError: string | null
onReopenReview: () => void
}): React.JSX.Element {
return (
<div className="space-y-1.5">
<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={onReopenReview}
disabled={stateUpdating !== null}
>
{stateUpdating === 'open' ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : (
<CircleDot className="size-3.5" />
)}
{stateUpdating === 'open'
? translate(
'auto.components.right.sidebar.HostedReviewActions.6645ac7dd1',
'Reopening...'
)
: translate(
'auto.components.right.sidebar.HostedReviewActions.3ce211ece6',
'Reopen {{value0}}',
{ value0: shortLabel }
)}
</Button>
<HostedReviewActionError message={actionError} />
</div>
)
}
export function MergedReviewActions({
isDeletingWorktree,
onDeleteWorktree
}: {
isDeletingWorktree: boolean
onDeleteWorktree: () => void
}): 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={onDeleteWorktree}
disabled={isDeletingWorktree}
>
{isDeletingWorktree ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : (
<Trash2 className="size-3.5" />
)}
{isDeletingWorktree
? translate('auto.components.right.sidebar.HostedReviewActions.eefd50457e', 'Deleting...')
: translate(
'auto.components.right.sidebar.HostedReviewActions.e4aca40024',
'Delete Workspace'
)}
</Button>
)
}
@@ -25,6 +25,7 @@ import {
isMissingMcpConfigFileError,
selectMcpTargetWorktree
} from './mcp-config-inspection-state'
import { McpMissingConfigList } from './McpMissingConfigList'
import { translate } from '@/i18n/i18n'
type McpConfigSectionProps = {
@@ -395,23 +396,7 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
</div>
)}
{missingConfigs.length > 0 && !inspectionUnavailable ? (
<div className="space-y-1.5 border-t border-border/50 px-3 py-2">
<p className="text-[11px] text-muted-foreground">
{translate('auto.components.settings.McpConfigSection.4d16a0d9ac', 'Checked')}
</p>
<div className="flex flex-wrap gap-1.5">
{missingConfigs.map((config) => (
<span
key={config.candidate.relativePath}
className="rounded-md border border-border/50 bg-background/40 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground"
>
{config.candidate.relativePath}
</span>
))}
</div>
</div>
) : null}
{!inspectionUnavailable ? <McpMissingConfigList missingConfigs={missingConfigs} /> : null}
</div>
</div>
</section>
@@ -0,0 +1,30 @@
import type { LoadedMcpConfigInspection } from './McpConfigFileRow'
import { translate } from '@/i18n/i18n'
export function McpMissingConfigList({
missingConfigs
}: {
missingConfigs: LoadedMcpConfigInspection[]
}): React.JSX.Element | null {
if (missingConfigs.length === 0) {
return null
}
return (
<div className="space-y-1.5 border-t border-border/50 px-3 py-2">
<p className="text-[11px] text-muted-foreground">
{translate('auto.components.settings.McpConfigSection.4d16a0d9ac', 'Checked')}
</p>
<div className="flex flex-wrap gap-1.5">
{missingConfigs.map((config) => (
<span
key={config.candidate.relativePath}
className="rounded-md border border-border/50 bg-background/40 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground"
>
{config.candidate.relativePath}
</span>
))}
</div>
</div>
)
}
@@ -106,8 +106,8 @@ describe('AddRepoDialogStepContent nested imports', () => {
const html = renderNestedStep(0)
expect(html).toContain('Is this a monorepo?')
expect(html).toContain('aria-label="Monorepo name"')
expect(html).toContain('Yes, import as monorepo')
expect(html).toContain('aria-label="Group name"')
expect(html).toContain('Import as group')
expect(html).toContain('No, import separately')
expect(html).not.toContain('>Import</button>')
})
@@ -116,8 +116,8 @@ describe('AddRepoDialogStepContent nested imports', () => {
const html = renderNestedStep(1)
expect(html).toContain('Is this a monorepo?')
expect(html).toContain('aria-label="Monorepo name"')
expect(html).toContain('Yes, import as monorepo')
expect(html).toContain('aria-label="Group name"')
expect(html).toContain('Import as group')
expect(html).toContain('No, import separately')
expect(html).not.toContain('>Import</button>')
})
@@ -1,6 +1,6 @@
// @vitest-environment happy-dom
import { act, type ComponentProps } from 'react'
import { act, useState, type ComponentProps } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, describe, expect, it, vi } from 'vitest'
@@ -78,13 +78,13 @@ describe('AddRepoNestedImportStep', () => {
expect(html).toContain('Import repositories from folder')
expect(html).toContain('Found 3 repositories in')
expect(html).toContain('/workspace/platform')
expect(html).toContain('aria-label="Monorepo name"')
expect(html).toContain('aria-label="What is a monorepo name?"')
expect(html).toContain('aria-label="Group name"')
expect(html).not.toContain('What is a')
expect(html).toContain('Is this a monorepo?')
expect(html).toContain('Choose this if these projects belong together')
expect(html).toContain('Import them as a group if they&#x27;re a monorepo')
expect(html).toContain('Orca will group them and let you work from the parent folder')
expect(html).toContain('No, import separately')
expect(html).toContain('Yes, import as monorepo')
expect(html).toContain('Import as group')
expect(html).toContain('payments/api')
expect(html).toContain('billing/api')
expect(html).not.toContain('disabled=""')
@@ -97,9 +97,9 @@ describe('AddRepoNestedImportStep', () => {
expect(html).toContain('Is this a monorepo?')
expect(html).toContain('No, import separately')
expect(html).toContain('Yes, import as monorepo')
expect(html).toContain('Import as group')
expect(html).toMatch(/<button[^>]*disabled=""[^>]*>No, import separately<\/button>/)
expect(html).toMatch(/<button[^>]*disabled=""[^>]*>Yes, import as monorepo<\/button>/)
expect(html).toMatch(/<button[^>]*disabled=""[^>]*>Import as group<\/button>/)
})
it('maps the monorepo choice to grouped import and the non-monorepo choice to separate import', () => {
@@ -130,11 +130,55 @@ describe('AddRepoNestedImportStep', () => {
})
act(() => {
findButton(host, 'Yes, import as monorepo').click()
findButton(host, 'Import as group').click()
findButton(host, 'No, import separately').click()
})
expect(onImport).toHaveBeenNthCalledWith(1, 'group')
expect(onImport).toHaveBeenNthCalledWith(2, 'separate')
})
it('shows progress only on the clicked import action', () => {
const onImport = vi.fn()
const host = document.createElement('div')
container = host
document.body.appendChild(host)
root = createRoot(host)
function Harness(): React.JSX.Element {
const [isAdding, setIsAdding] = useState(false)
return (
<TooltipProvider>
<Dialog open>
<AddRepoNestedImportStep
scan={scan}
groupName=""
selectedPaths={new Set(scan.repos.map((repo) => repo.path))}
isAdding={isAdding}
scanInProgress={false}
onGroupNameChange={vi.fn()}
onSelectedPathsChange={vi.fn()}
onImport={(mode) => {
onImport(mode)
setIsAdding(true)
}}
onStopScan={vi.fn()}
/>
</Dialog>
</TooltipProvider>
)
}
act(() => {
root?.render(<Harness />)
})
act(() => {
findButton(host, 'Import as group').click()
})
expect(onImport).toHaveBeenCalledWith('group')
expect(findButton(host, 'Import as group').querySelector('.animate-spin')).not.toBeNull()
expect(findButton(host, 'No, import separately').querySelector('.animate-spin')).toBeNull()
})
})
@@ -1,5 +1,5 @@
import { useId, type Dispatch, type SetStateAction } from 'react'
import { CircleHelp, CircleStop, Loader2 } from 'lucide-react'
import { useEffect, useId, useState, type Dispatch, type SetStateAction } from 'react'
import { CircleStop, Loader2 } from 'lucide-react'
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -36,6 +36,20 @@ export function AddRepoNestedImportStep({
}: AddRepoNestedImportStepProps): React.JSX.Element {
const folderName = getRuntimePathBasename(scan.selectedPath) || scan.selectedPath
const groupNameInputId = useId()
const [pendingImportMode, setPendingImportMode] = useState<'group' | 'separate' | null>(null)
const showSeparateSpinner = isAdding && pendingImportMode === 'separate'
const showGroupSpinner = isAdding && pendingImportMode === 'group'
useEffect(() => {
if (!isAdding) {
setPendingImportMode(null)
}
}, [isAdding])
const handleImport = (mode: 'group' | 'separate'): void => {
setPendingImportMode(mode)
onImport(mode)
}
const repoCountLabel =
scan.repos.length === 1
? translate('auto.components.sidebar.AddRepoNestedImportStep.8401a7a0d0', '1 repository')
@@ -97,7 +111,7 @@ export function AddRepoNestedImportStep({
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.sidebar.AddRepoNestedImportStep.d75170194e',
'Choose this if these projects belong together. Orca will group them and let you work from the parent folder.'
"Import them as a group if they're a monorepo or otherwise belong together. Orca will group them and let you work from the parent folder."
)}
</p>
</div>
@@ -106,37 +120,15 @@ export function AddRepoNestedImportStep({
<Label htmlFor={groupNameInputId} className="text-[11px] text-muted-foreground">
{translate(
'auto.components.sidebar.AddRepoNestedImportStep.39d51212cc',
'Monorepo name'
'Group name'
)}
</Label>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={translate(
'auto.components.sidebar.AddRepoNestedImportStep.e907ec8935',
'What is a monorepo name?'
)}
className="size-5 text-muted-foreground hover:text-foreground"
>
<CircleHelp className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4} className="max-w-64">
{translate(
'auto.components.sidebar.AddRepoNestedImportStep.b20bb7c24f',
'Keeps these repos together in one group. Best for related repos like microservices.'
)}
</TooltipContent>
</Tooltip>
</div>
<Input
id={groupNameInputId}
aria-label={translate(
'auto.components.sidebar.AddRepoNestedImportStep.39d51212cc',
'Monorepo name'
'Group name'
)}
value={groupName}
onChange={(event) => onGroupNameChange(event.target.value)}
@@ -147,22 +139,24 @@ export function AddRepoNestedImportStep({
</div>
<div className="flex shrink-0 flex-wrap justify-end gap-2">
<Button
onClick={() => onImport('separate')}
onClick={() => handleImport('separate')}
disabled={isAdding || scanInProgress || selectedPaths.size === 0}
variant="outline"
>
{showSeparateSpinner ? <Loader2 className="size-3.5 animate-spin" /> : null}
{translate(
'auto.components.sidebar.AddRepoNestedImportStep.aa0247680d',
'No, import separately'
)}
</Button>
<Button
onClick={() => onImport('group')}
onClick={() => handleImport('group')}
disabled={isAdding || scanInProgress || selectedPaths.size === 0}
>
{showGroupSpinner ? <Loader2 className="size-3.5 animate-spin" /> : null}
{translate(
'auto.components.sidebar.AddRepoNestedImportStep.a0bc4d1f8e',
'Yes, import as monorepo'
'Import as group'
)}
</Button>
</div>
@@ -335,17 +335,10 @@ export function FolderWorkspaceComposerDialog({
repoId={repoId}
selectedRepoIsGit={true}
onRepoChange={handleRepoChange}
primaryActionLabel={
quickAgent
? translate(
'auto.components.sidebar.FolderWorkspaceComposerDialog.createStart',
'Create & Start Agent'
)
: translate(
'auto.components.sidebar.FolderWorkspaceComposerDialog.create',
'Create Workspace'
)
}
primaryActionLabel={translate(
'auto.components.sidebar.FolderWorkspaceComposerDialog.create',
'Create workspace'
)}
projectLabel={translate(
'auto.components.sidebar.FolderWorkspaceComposerDialog.sourceProject',
'Task Source'
@@ -15,6 +15,16 @@ type WorktreeListComponent = React.ComponentType<{
let WorktreeList: WorktreeListComponent
function makeFolderWorkspacePathStatusMockState(): Record<string, unknown> {
return {
fetchFolderWorkspacePathStatus: vi.fn(),
folderWorkspaces: [],
folderWorkspacePathStatuses: {},
getFolderWorkspacePathStatusCacheKey: (request: unknown) => JSON.stringify(request),
getFreshFolderWorkspacePathStatus: () => null
}
}
vi.mock('@/store', () => {
const useAppStore = ((selector: (state: Record<string, unknown>) => unknown) =>
selector(mockStore.state)) as ((
@@ -316,6 +326,7 @@ function setLineageFixtureState(
grandchild.isUnread = unreadWorktreeIds.has(grandchild.id)
mockStore.state = {
...makeFolderWorkspacePathStatusMockState(),
activeModal: '',
activeView: 'terminal',
activeWorktreeId: null,
@@ -396,6 +407,7 @@ function setProjectGroupWithoutWorktreeRowsState(filterRepoIds: string[] = []):
}
mockStore.state = {
...makeFolderWorkspacePathStatusMockState(),
activeModal: '',
activeView: 'terminal',
activeWorktreeId: null,
@@ -456,6 +468,7 @@ function setEmptyUngroupedProjectState(filterRepoIds: string[] = []): void {
}
mockStore.state = {
...makeFolderWorkspacePathStatusMockState(),
activeModal: '',
activeView: 'terminal',
activeWorktreeId: null,
@@ -31,6 +31,16 @@ type WorktreeListComponent = React.ComponentType<{
let WorktreeList: WorktreeListComponent
function makeFolderWorkspacePathStatusMockState(): Record<string, unknown> {
return {
fetchFolderWorkspacePathStatus: vi.fn(),
folderWorkspaces: [],
folderWorkspacePathStatuses: {},
getFolderWorkspacePathStatusCacheKey: (request: unknown) => JSON.stringify(request),
getFreshFolderWorkspacePathStatus: () => null
}
}
vi.mock('@/store', () => {
const useAppStore = ((selector: (state: Record<string, unknown>) => unknown) =>
selector(mockStore.state)) as ((
@@ -234,6 +244,7 @@ function setLineageState(options: { deletingChild?: boolean } = {}): void {
}
})
mockStore.state = {
...makeFolderWorkspacePathStatusMockState(),
activeModal: '',
activeView: 'terminal',
activeWorktreeId: null,
@@ -0,0 +1,91 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types'
const mocks = vi.hoisted(() => ({
activateAndRevealFolderWorkspace: vi.fn()
}))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealFolderWorkspace: mocks.activateAndRevealFolderWorkspace
}))
import { submitFolderWorkspaceCreate } from './folder-workspace-composer-submit'
function makeProjectGroup(): ProjectGroup {
return {
id: 'group-1',
name: 'Platform',
parentPath: '/repo/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
}
function makeFolderWorkspace(): FolderWorkspace {
return {
id: 'folder-workspace-1',
projectGroupId: 'group-1',
name: 'hi',
folderPath: '/repo/platform/hi',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
}
describe('submitFolderWorkspaceCreate', () => {
afterEach(() => {
mocks.activateAndRevealFolderWorkspace.mockReset()
vi.restoreAllMocks()
})
it('closes the composer after creation even when reveal fails', async () => {
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
const onOpenChange = vi.fn()
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
mocks.activateAndRevealFolderWorkspace.mockImplementation(() => {
throw new Error('activation failed')
})
await submitFolderWorkspaceCreate({
projectGroup: makeProjectGroup(),
name: 'hi',
lastAutoName: '',
linkedWorkItem: null,
note: '',
quickAgent: null,
autoRenameBranchFromWork: false,
agentCmdOverrides: {},
createFolderWorkspace,
onOpenChange
})
expect(createFolderWorkspace).toHaveBeenCalledWith({
projectGroupId: 'group-1',
name: 'hi',
linkedTask: null
})
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith(
'folder-workspace-1',
undefined
)
expect(consoleError).toHaveBeenCalledWith(
'Failed to activate folder workspace after create:',
expect.any(Error)
)
})
})
@@ -4,6 +4,7 @@ import {
type LinkedWorkItemSummary
} from '@/lib/new-workspace'
import { getLinkedWorkItemPromptContext } from '@/lib/linked-work-item-context'
import { isOrcaCliAvailableForLaunch } from '@/lib/orca-cli-launch-availability'
import { buildAgentStartupPlan } from '@/lib/tui-agent-startup'
import { tuiAgentToAgentKind } from '@/lib/telemetry'
import { activateAndRevealFolderWorkspace } from '@/lib/worktree-activation'
@@ -67,8 +68,11 @@ export async function submitFolderWorkspaceCreate({
return
}
const linearCliAvailable = linkedWorkItem?.linearIdentifier
? await isOrcaCliAvailableForLaunch({ remote: projectGroup.connectionId != null })
: false
const linkedPromptContext = getLinkedWorkItemPromptContext(linkedWorkItem, {
cliAvailable: false
cliAvailable: linearCliAvailable
})
const startupPrompt = buildAgentPromptWithContext(
note,
@@ -97,6 +101,12 @@ export async function submitFolderWorkspaceCreate({
}
}
: undefined
activateAndRevealFolderWorkspace(workspace.id, startup ? { startup } : undefined)
onOpenChange(false)
try {
activateAndRevealFolderWorkspace(workspace.id, startup ? { startup } : undefined)
} catch (error) {
// Why: creation already succeeded. Do not leave the completed create modal
// open if the follow-up reveal/startup path hits a transient issue.
console.error('Failed to activate folder workspace after create:', error)
}
}
@@ -12,16 +12,12 @@ import type { ClaudeUsageRange, ClaudeUsageScope } from '../../../../shared/clau
import { useAppStore } from '../../store'
import { ClaudeUsageDailyChart } from './ClaudeUsageDailyChart'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { ClaudeUsageRecentSessionsTable } from './ClaudeUsageRecentSessionsTable'
import { ShareUsageButton } from './ShareUsageButton'
import { StatCard } from './StatCard'
import { UsagePaneFilterControls } from './UsagePaneFilterControls'
import { UsageTrackingDisabledCard } from './UsageTrackingDisabledCard'
import {
formatCost,
formatSessionTime,
formatTokens,
formatUpdatedAt
} from './usage-display-formatting'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-display-formatting'
import { translate } from '@/i18n/i18n'
const RANGE_OPTIONS: ClaudeUsageRange[] = ['7d', '30d', '90d', 'all']
@@ -292,72 +288,7 @@ export function ClaudeUsagePane(): React.JSX.Element {
</section>
</div>
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.ClaudeUsagePane.7e76c84153', 'Recent sessions')}
</h4>
<p className="text-xs text-muted-foreground">
{translate('auto.components.stats.ClaudeUsagePane.abfc4a4943', 'Cache reuse rate:')}{' '}
{summary?.cacheReuseRate !== null && summary?.cacheReuseRate !== undefined
? `${Math.round(summary.cacheReuseRate * 100)}%`
: translate('auto.components.stats.ClaudeUsagePane.7765a4c3e1', 'n/a')}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-border/60 text-left text-xs text-muted-foreground">
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.01476891c7', 'Last active')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.c17bed0416', 'Project')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.1afc25eb06', 'Model')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.0f03975d59', 'Turns')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.faf3444859', 'Input')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.a8b7487ff7', 'Output')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.21ea00bfa8', 'Cache')}
</th>
</tr>
</thead>
<tbody>
{recentSessions.map((row) => (
<tr key={row.sessionId} className="border-b border-border/40 last:border-b-0">
<td className="px-2 py-2 text-muted-foreground">
{formatSessionTime(row.lastActiveAt)}
</td>
<td className="px-2 py-2 text-foreground">{row.projectLabel}</td>
<td className="px-2 py-2 text-muted-foreground">
{row.model ??
translate('auto.components.stats.ClaudeUsagePane.cfe2282ffa', 'Unknown')}
</td>
<td className="px-2 py-2 text-muted-foreground">{row.turns}</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.inputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.outputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.cacheReadTokens + row.cacheWriteTokens)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<ClaudeUsageRecentSessionsTable recentSessions={recentSessions} summary={summary} />
</>
)}
</div>
@@ -0,0 +1,81 @@
import type {
ClaudeUsageSessionRow,
ClaudeUsageSummary
} from '../../../../shared/claude-usage-types'
import { translate } from '@/i18n/i18n'
import { formatSessionTime, formatTokens } from './usage-formatters'
export function ClaudeUsageRecentSessionsTable({
recentSessions,
summary
}: {
recentSessions: ClaudeUsageSessionRow[]
summary: ClaudeUsageSummary | null
}): React.JSX.Element {
return (
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.ClaudeUsagePane.7e76c84153', 'Recent sessions')}
</h4>
<p className="text-xs text-muted-foreground">
{translate('auto.components.stats.ClaudeUsagePane.abfc4a4943', 'Cache reuse rate:')}{' '}
{summary?.cacheReuseRate !== null && summary?.cacheReuseRate !== undefined
? `${Math.round(summary.cacheReuseRate * 100)}%`
: translate('auto.components.stats.ClaudeUsagePane.7765a4c3e1', 'n/a')}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-border/60 text-left text-xs text-muted-foreground">
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.01476891c7', 'Last active')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.c17bed0416', 'Project')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.1afc25eb06', 'Model')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.0f03975d59', 'Turns')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.faf3444859', 'Input')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.a8b7487ff7', 'Output')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.ClaudeUsagePane.21ea00bfa8', 'Cache')}
</th>
</tr>
</thead>
<tbody>
{recentSessions.map((row) => (
<tr key={row.sessionId} className="border-b border-border/40 last:border-b-0">
<td className="px-2 py-2 text-muted-foreground">
{formatSessionTime(row.lastActiveAt)}
</td>
<td className="px-2 py-2 text-foreground">{row.projectLabel}</td>
<td className="px-2 py-2 text-muted-foreground">
{row.model ??
translate('auto.components.stats.ClaudeUsagePane.cfe2282ffa', 'Unknown')}
</td>
<td className="px-2 py-2 text-muted-foreground">{row.turns}</td>
<td className="px-2 py-2 text-muted-foreground">{formatTokens(row.inputTokens)}</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.outputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.cacheReadTokens + row.cacheWriteTokens)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)
}
@@ -4,16 +4,12 @@ import type { CodexUsageRange, CodexUsageScope } from '../../../../shared/codex-
import { useAppStore } from '../../store'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { CodexUsageDailyChart } from './CodexUsageDailyChart'
import { CodexUsageRecentSessionsTable } from './CodexUsageRecentSessionsTable'
import { ShareUsageButton } from './ShareUsageButton'
import { StatCard } from './StatCard'
import { UsagePaneFilterControls } from './UsagePaneFilterControls'
import { UsageTrackingDisabledCard } from './UsageTrackingDisabledCard'
import {
formatCost,
formatSessionTime,
formatTokens,
formatUpdatedAt
} from './usage-display-formatting'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-display-formatting'
import { translate } from '@/i18n/i18n'
const RANGE_OPTIONS: CodexUsageRange[] = ['7d', '30d', '90d', 'all']
@@ -269,73 +265,7 @@ export function CodexUsagePane(): React.JSX.Element {
</section>
</div>
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.CodexUsagePane.0cb0983c07', 'Recent sessions')}
</h4>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.stats.CodexUsagePane.0bd8655475',
'Most recent local Codex sessions in this scope.'
)}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-border/60 text-left text-xs text-muted-foreground">
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.0c36b100be', 'Last active')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.1a65900aea', 'Project')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.c2478bcc3c', 'Model')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.bd0822ca47', 'Events')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.3acc582214', 'Input')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.bbd20344b8', 'Output')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.e0b988599d', 'Total')}
</th>
</tr>
</thead>
<tbody>
{recentSessions.map((row) => (
<tr key={row.sessionId} className="border-b border-border/40 last:border-b-0">
<td className="px-2 py-2 text-muted-foreground">
{formatSessionTime(row.lastActiveAt)}
</td>
<td className="px-2 py-2 text-foreground">{row.projectLabel}</td>
<td className="px-2 py-2 text-muted-foreground">
{row.model ??
translate('auto.components.stats.CodexUsagePane.bf6cf2d4dd', 'Unknown')}
{row.hasInferredPricing ? ' *' : ''}
</td>
<td className="px-2 py-2 text-muted-foreground">{row.events}</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.inputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.outputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.totalTokens)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<CodexUsageRecentSessionsTable recentSessions={recentSessions} />
</>
)}
</div>
@@ -0,0 +1,75 @@
import type { CodexUsageSessionRow } from '../../../../shared/codex-usage-types'
import { translate } from '@/i18n/i18n'
import { formatSessionTime, formatTokens } from './usage-formatters'
export function CodexUsageRecentSessionsTable({
recentSessions
}: {
recentSessions: CodexUsageSessionRow[]
}): React.JSX.Element {
return (
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.CodexUsagePane.0cb0983c07', 'Recent sessions')}
</h4>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.stats.CodexUsagePane.0bd8655475',
'Most recent local Codex sessions in this scope.'
)}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-border/60 text-left text-xs text-muted-foreground">
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.0c36b100be', 'Last active')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.1a65900aea', 'Project')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.c2478bcc3c', 'Model')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.bd0822ca47', 'Events')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.3acc582214', 'Input')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.bbd20344b8', 'Output')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.CodexUsagePane.e0b988599d', 'Total')}
</th>
</tr>
</thead>
<tbody>
{recentSessions.map((row) => (
<tr key={row.sessionId} className="border-b border-border/40 last:border-b-0">
<td className="px-2 py-2 text-muted-foreground">
{formatSessionTime(row.lastActiveAt)}
</td>
<td className="px-2 py-2 text-foreground">{row.projectLabel}</td>
<td className="px-2 py-2 text-muted-foreground">
{row.model ??
translate('auto.components.stats.CodexUsagePane.bf6cf2d4dd', 'Unknown')}
{row.hasInferredPricing ? ' *' : ''}
</td>
<td className="px-2 py-2 text-muted-foreground">{row.events}</td>
<td className="px-2 py-2 text-muted-foreground">{formatTokens(row.inputTokens)}</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.outputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">{formatTokens(row.totalTokens)}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)
}
@@ -7,15 +7,11 @@ import type {
import { useAppStore } from '../../store'
import { ClaudeUsageLoadingState } from './ClaudeUsageLoadingState'
import { CodexUsageDailyChart } from './CodexUsageDailyChart'
import { OpenCodeUsageRecentSessionsTable } from './OpenCodeUsageRecentSessionsTable'
import { StatCard } from './StatCard'
import { UsagePaneFilterControls } from './UsagePaneFilterControls'
import { UsageTrackingDisabledCard } from './UsageTrackingDisabledCard'
import {
formatCost,
formatSessionTime,
formatTokens,
formatUpdatedAt
} from './usage-display-formatting'
import { formatCost, formatTokens, formatUpdatedAt } from './usage-display-formatting'
import { translate } from '@/i18n/i18n'
const RANGE_OPTIONS: OpenCodeUsageRange[] = ['7d', '30d', '90d', 'all']
@@ -293,78 +289,7 @@ export function OpenCodeUsagePane(): React.JSX.Element {
</section>
</div>
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.OpenCodeUsagePane.4799177b1c', 'Recent sessions')}
</h4>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.stats.OpenCodeUsagePane.81817a641a',
'Most recent local OpenCode sessions in this scope.'
)}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-border/60 text-left text-xs text-muted-foreground">
<th className="px-2 py-2 font-medium">
{translate(
'auto.components.stats.OpenCodeUsagePane.d97bdf6e27',
'Last active'
)}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.a4738de041', 'Project')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.08c78441b7', 'Model')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.d416f5cf92', 'Events')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.0f2f266c9d', 'Input')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.dfc4513657', 'Output')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.349f7c3f5c', 'Total')}
</th>
</tr>
</thead>
<tbody>
{recentSessions.map((row) => (
<tr key={row.sessionId} className="border-b border-border/40 last:border-b-0">
<td className="px-2 py-2 text-muted-foreground">
{formatSessionTime(row.lastActiveAt)}
</td>
<td className="px-2 py-2 text-foreground">{row.projectLabel}</td>
<td className="px-2 py-2 text-muted-foreground">
{row.model ??
translate(
'auto.components.stats.OpenCodeUsagePane.362231082f',
'Unknown'
)}
</td>
<td className="px-2 py-2 text-muted-foreground">{row.events}</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.inputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.outputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.totalTokens)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<OpenCodeUsageRecentSessionsTable recentSessions={recentSessions} />
</>
)}
</div>
@@ -0,0 +1,74 @@
import type { OpenCodeUsageSessionRow } from '../../../../shared/opencode-usage-types'
import { translate } from '@/i18n/i18n'
import { formatSessionTime, formatTokens } from './usage-formatters'
export function OpenCodeUsageRecentSessionsTable({
recentSessions
}: {
recentSessions: OpenCodeUsageSessionRow[]
}): React.JSX.Element {
return (
<section className="rounded-lg border border-border/60 bg-card/40 p-4">
<div className="mb-3">
<h4 className="text-sm font-semibold text-foreground">
{translate('auto.components.stats.OpenCodeUsagePane.4799177b1c', 'Recent sessions')}
</h4>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.stats.OpenCodeUsagePane.81817a641a',
'Most recent local OpenCode sessions in this scope.'
)}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-border/60 text-left text-xs text-muted-foreground">
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.d97bdf6e27', 'Last active')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.a4738de041', 'Project')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.08c78441b7', 'Model')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.d416f5cf92', 'Events')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.0f2f266c9d', 'Input')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.dfc4513657', 'Output')}
</th>
<th className="px-2 py-2 font-medium">
{translate('auto.components.stats.OpenCodeUsagePane.349f7c3f5c', 'Total')}
</th>
</tr>
</thead>
<tbody>
{recentSessions.map((row) => (
<tr key={row.sessionId} className="border-b border-border/40 last:border-b-0">
<td className="px-2 py-2 text-muted-foreground">
{formatSessionTime(row.lastActiveAt)}
</td>
<td className="px-2 py-2 text-foreground">{row.projectLabel}</td>
<td className="px-2 py-2 text-muted-foreground">
{row.model ??
translate('auto.components.stats.OpenCodeUsagePane.362231082f', 'Unknown')}
</td>
<td className="px-2 py-2 text-muted-foreground">{row.events}</td>
<td className="px-2 py-2 text-muted-foreground">{formatTokens(row.inputTokens)}</td>
<td className="px-2 py-2 text-muted-foreground">
{formatTokens(row.outputTokens)}
</td>
<td className="px-2 py-2 text-muted-foreground">{formatTokens(row.totalTokens)}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)
}
@@ -0,0 +1,36 @@
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'
})
}
+4 -7
View File
@@ -3085,7 +3085,6 @@
"2f8298f3c3": "Stop scan",
"c157f31a95": "Import as group",
"40199ef7b3": "Group name",
"b20bb7c24f": "Keeps these repos together in one group. Best for related repos like microservices.",
"787412361a": "What is a group name?",
"5f857ba8e6": "in",
"4df0d08cc5": "Found",
@@ -3094,11 +3093,10 @@
"cf9d382ca1": "Import",
"220dd32d83": "Scanning...",
"fb33359f69": "Is this a monorepo?",
"d75170194e": "Choose this if these projects belong together. Orca will group them and let you work from the parent folder.",
"39d51212cc": "Monorepo name",
"e907ec8935": "What is a monorepo name?",
"d75170194e": "Import them as a group if they're a monorepo or otherwise belong together. Orca will group them and let you work from the parent folder.",
"39d51212cc": "Group name",
"aa0247680d": "No, import separately",
"a0bc4d1f8e": "Yes, import as monorepo",
"a0bc4d1f8e": "Import as group",
"8401a7a0d0": "1 repository",
"d4f1df62ef": "{{value0}} repositories",
"b4263a2ac4": "Found {{value0}} in {{value1}}.",
@@ -3848,8 +3846,7 @@
"connectFailed": "Failed to connect to project.",
"noRepos": "Add a Git project under this folder to attach GitHub or GitLab tasks.",
"title": "Create Folder Workspace",
"createStart": "Create & Start Agent",
"create": "Create Workspace",
"create": "Create workspace",
"sourceProject": "Task Source",
"chooseSourceProject": "Choose task source"
}
+4 -7
View File
@@ -3085,7 +3085,6 @@
"2f8298f3c3": "Detener escaneo",
"c157f31a95": "Importar como grupo",
"40199ef7b3": "Nombre del grupo",
"b20bb7c24f": "Mantiene estos repos juntos en un grupo. Lo mejor para repos relacionados como microservicios.",
"787412361a": "¿Qué es el nombre de un grupo?",
"5f857ba8e6": "en",
"4df0d08cc5": "Encontró",
@@ -3094,11 +3093,10 @@
"cf9d382ca1": "Importar",
"220dd32d83": "Exploración...",
"fb33359f69": "¿Es esto un monorepo?",
"d75170194e": "Elige esto si estos proyectos pertenecen juntos. Orca los agrupará y te permitirá trabajar desde la carpeta principal.",
"39d51212cc": "Nombre del monorepo",
"e907ec8935": "¿Qué es un nombre de monorepo?",
"d75170194e": "Impórtalos como grupo si son un monorepo o si pertenecen juntos. Orca los agrupará y te permitirá trabajar desde la carpeta principal.",
"39d51212cc": "Nombre del grupo",
"aa0247680d": "No, importar por separado",
"a0bc4d1f8e": "Sí, importar como monorepo",
"a0bc4d1f8e": "Importar como grupo",
"8401a7a0d0": "1 repositorio",
"d4f1df62ef": "{{value0}} repositorios",
"b4263a2ac4": "Se encontraron {{value0}} en {{value1}}.",
@@ -3848,8 +3846,7 @@
"connectFailed": "Failed to connect to project.",
"noRepos": "Add a Git project under this folder to attach GitHub or GitLab tasks.",
"title": "Create Folder Workspace",
"createStart": "Create & Start Agent",
"create": "Create Workspace",
"create": "Create workspace",
"sourceProject": "Task Source",
"chooseSourceProject": "Choose task source"
}
File diff suppressed because it is too large Load Diff
+5 -8
View File
@@ -3066,7 +3066,6 @@
"2f8298f3c3": "스캔 중지",
"c157f31a95": "그룹으로 가져오기",
"40199ef7b3": "그룹 이름",
"b20bb7c24f": "이러한 repos를 하나의 그룹에 함께 유지합니다. 마이크로서비스와 같은 관련 repos 에 가장 적합합니다.",
"787412361a": "그룹 이름은 무엇입니까?",
"5f857ba8e6": "~에",
"4df0d08cc5": "발견됨",
@@ -3075,11 +3074,10 @@
"cf9d382ca1": "수입",
"220dd32d83": "스캐닝...",
"fb33359f69": "이 폴더가 모노레포인가요?",
"d75170194e": "이 프로젝트들이 함께 속한다면 이것을 선택하세요. Orca가 그룹으로 묶고 상위 폴더에서 작업할 수 있게 합니다.",
"39d51212cc": "모노레포 이름",
"e907ec8935": "모노레포 이름이란 무엇인가요?",
"d75170194e": "모노레포이거나 함께 속한 프로젝트라면 그룹으로 가져오세요. Orca가 그룹으로 묶고 상위 폴더에서 작업할 수 있게 합니다.",
"39d51212cc": "그룹 이름",
"aa0247680d": "아니요, 별도로 가져오기",
"a0bc4d1f8e": "예, 모노레포로 가져오기",
"a0bc4d1f8e": "그룹으로 가져오기",
"8401a7a0d0": "저장소 1개",
"d4f1df62ef": "저장소 {{value0}}개",
"b4263a2ac4": "{{value1}}에서 {{value0}}을(를) 찾았습니다.",
@@ -3598,7 +3596,7 @@
"c1f4a31623": "{{value0}}개 하위 워크스페이스 표시",
"e97297cb75": "{{value0}}개 하위 워크스페이스 숨기기",
"0cd15956d4": "{{value0}}개 하위 워크스페이스 숨기기",
"bd37a57ac8": "Create workspace for {{value0}}",
"bd37a57ac8": "{{value0}}에 대한 워크스페이스 만들기",
"b667b59632": "Some projects could not be removed from Orca",
"f94466bc39": "{{value0}} of {{value1}} contained project{{value2}} remained after deleting the group.",
"groupDeleteFailed": "Failed to delete group",
@@ -3848,8 +3846,7 @@
"connectFailed": "Failed to connect to project.",
"noRepos": "Add a Git project under this folder to attach GitHub or GitLab tasks.",
"title": "Create Folder Workspace",
"createStart": "Create & Start Agent",
"create": "Create Workspace",
"create": "워크스페이스 만들기",
"sourceProject": "Task Source",
"chooseSourceProject": "Choose task source"
}
+5 -8
View File
@@ -3066,7 +3066,6 @@
"2f8298f3c3": "停止扫描",
"c157f31a95": "作为组导入",
"40199ef7b3": "团体名称",
"b20bb7c24f": "将这些 repos 放在一组中。最适合微服务等相关 repos。",
"787412361a": "什么是群组名称?",
"5f857ba8e6": "在",
"4df0d08cc5": "成立",
@@ -3075,11 +3074,10 @@
"cf9d382ca1": "导入",
"220dd32d83": "扫描...",
"fb33359f69": "这是 Monorepo 吗?",
"d75170194e": "如果这些项目属于同一组,请选择此项。Orca 会将它们分组,并让你从父文件夹开始工作。",
"39d51212cc": "Monorepo 名称",
"e907ec8935": "什么是 Monorepo 名称?",
"d75170194e": "如果它们是 Monorepo,或本来就应该放在一起,请将它们作为组导入。Orca 会将它们分组,并让你从父文件夹开始工作。",
"39d51212cc": "组名称",
"aa0247680d": "否,单独导入",
"a0bc4d1f8e": "是,作为 Monorepo 导入",
"a0bc4d1f8e": "作为组导入",
"8401a7a0d0": "1 个仓库",
"d4f1df62ef": "{{value0}} 个仓库",
"b4263a2ac4": "在 {{value1}} 中找到 {{value0}}。",
@@ -3598,7 +3596,7 @@
"c1f4a31623": "显示 {{value0}} 个子工作区",
"e97297cb75": "隐藏 {{value0}} 个子工作区",
"0cd15956d4": "隐藏 {{value0}} 个子工作区",
"bd37a57ac8": "Create workspace for {{value0}}",
"bd37a57ac8": "为 {{value0}} 创建工作区",
"b667b59632": "Some projects could not be removed from Orca",
"f94466bc39": "{{value0}} of {{value1}} contained project{{value2}} remained after deleting the group.",
"groupDeleteFailed": "Failed to delete group",
@@ -3848,8 +3846,7 @@
"connectFailed": "Failed to connect to project.",
"noRepos": "Add a Git project under this folder to attach GitHub or GitLab tasks.",
"title": "Create Folder Workspace",
"createStart": "Create & Start Agent",
"create": "Create Workspace",
"create": "创建工作区",
"sourceProject": "Task Source",
"chooseSourceProject": "Choose task source"
}