diff --git a/config/localization-coverage-allowlist.json b/config/localization-coverage-allowlist.json
index 2a3b4603f36..b37d3374bf5 100644
--- a/config/localization-coverage-allowlist.json
+++ b/config/localization-coverage-allowlist.json
@@ -6,6 +6,13 @@
"dynamic": false,
"count": 1
},
+ {
+ "filePath": "src/renderer/src/components/sidebar/WorktreeCard.tsx",
+ "kind": "jsx-attribute:label",
+ "text": "sidebar",
+ "dynamic": false,
+ "count": 1
+ },
{
"filePath": "src/renderer/src/components/settings/appearance-search.ts",
"kind": "object-property:keywords",
diff --git a/config/scripts/audit-localization-coverage.mjs b/config/scripts/audit-localization-coverage.mjs
index 8b978e38ed4..fd750a9150d 100644
--- a/config/scripts/audit-localization-coverage.mjs
+++ b/config/scripts/audit-localization-coverage.mjs
@@ -60,6 +60,14 @@ const USER_VISIBLE_OBJECT_METHODS = new Set([
'warning'
])
const USER_VISIBLE_OBJECT_NAMES = new Set(['toast'])
+// Why: only comparison operands are code, not copy. Bailing on every non-`+`
+// operator hid whole subtrees behind `cond && ` guards and `?? 'fallback'`.
+const COPY_PRESERVING_BINARY_OPERATORS = new Set([
+ ts.SyntaxKind.PlusToken,
+ ts.SyntaxKind.QuestionQuestionToken,
+ ts.SyntaxKind.BarBarToken,
+ ts.SyntaxKind.AmpersandAmpersandToken
+])
function normalizePath(root, filePath) {
return path.relative(root, filePath).split(path.sep).join('/')
@@ -226,7 +234,7 @@ function isRenderedJsxExpression(node) {
continue
}
if (ts.isBinaryExpression(current)) {
- if (current.operatorToken.kind !== ts.SyntaxKind.PlusToken) {
+ if (!COPY_PRESERVING_BINARY_OPERATORS.has(current.operatorToken.kind)) {
return false
}
current = current.parent
@@ -318,7 +326,8 @@ function classifyStringNode(node) {
findAncestor(
node,
(ancestor) =>
- ts.isBinaryExpression(ancestor) && ancestor.operatorToken.kind !== ts.SyntaxKind.PlusToken
+ ts.isBinaryExpression(ancestor) &&
+ !COPY_PRESERVING_BINARY_OPERATORS.has(ancestor.operatorToken.kind)
)
) {
return undefined
diff --git a/config/scripts/audit-localization-coverage.test.mjs b/config/scripts/audit-localization-coverage.test.mjs
new file mode 100644
index 00000000000..291e6e29595
--- /dev/null
+++ b/config/scripts/audit-localization-coverage.test.mjs
@@ -0,0 +1,44 @@
+import { describe, expect, it } from 'vitest'
+
+import { collectLocalizationCandidates } from './audit-localization-coverage.mjs'
+
+const ROOT = process.cwd()
+
+function candidates(fileName, source) {
+ return collectLocalizationCandidates(`${ROOT}/src/renderer/src/${fileName}`, source, ROOT)
+}
+
+describe('localization coverage candidates', () => {
+ it('sees copy guarded by a nullish or logical fallback', () => {
+ const reports = candidates(
+ 'Sample.tsx',
+ `export function Sample({ label, connecting }) {
+ return {label ?? (connecting ? 'Connecting…' : 'Idle')}
+ }`
+ )
+
+ expect(reports.map((report) => report.text)).toEqual(['Connecting…', 'Idle'])
+ })
+
+ it('sees copy nested inside a conditional JSX guard', () => {
+ const reports = candidates(
+ 'Sample.tsx',
+ `export function Sample({ show }) {
+ return
{show && }
+ }`
+ )
+
+ expect(reports.map((report) => report.text)).toEqual(['Retry the sync'])
+ })
+
+ it('ignores literals used as comparison operands', () => {
+ const reports = candidates(
+ 'Sample.tsx',
+ `export function Sample({ phase }) {
+ return {phase === 'workspace conflict' ? phase : null}
+ }`
+ )
+
+ expect(reports).toEqual([])
+ })
+})
diff --git a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx
index 039c2035d59..1e8ac06c414 100644
--- a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx
+++ b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx
@@ -280,7 +280,14 @@ export async function ensureWslCliAvailableForAgentSkillTerminal(
'auto.components.settings.CliSkillRuntimeSetup.windowsPathUnknown',
'WSL shell command PATH could not be checked'
),
- { description: status.detail ?? 'Refresh CLI registration status and try again.' }
+ {
+ description:
+ status.detail ??
+ translate(
+ 'auto.components.settings.CliSkillRuntimeSetup.refreshCliRegistration',
+ 'Refresh CLI registration status and try again.'
+ )
+ }
)
return status
}
diff --git a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx
index fe0f5e1a297..c4690a1c79a 100644
--- a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx
+++ b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx
@@ -1184,12 +1184,9 @@ export function ResourceUsageStatusSegment({
- {resourceManagerTooltipLines.map((line, index) => (
-
- {line}
+ {resourceManagerTooltipLines.map((line) => (
+
+ {line.text}
))}
diff --git a/src/renderer/src/components/status-bar/SshStatusSegment.tsx b/src/renderer/src/components/status-bar/SshStatusSegment.tsx
index 70e500ad09a..fc417d679f1 100644
--- a/src/renderer/src/components/status-bar/SshStatusSegment.tsx
+++ b/src/renderer/src/components/status-bar/SshStatusSegment.tsx
@@ -18,6 +18,11 @@ import {
} from '../../../../shared/execution-host'
import { isUserManagedRuntimeEnvironment } from '../../../../shared/runtime-environments'
import { RuntimeHostStatusRow, type RuntimeHostConnectionState } from './RuntimeHostStatusRow'
+import {
+ connectedHostCountLabel,
+ connectingHostsLabel,
+ workspaceSyncProblemLabel
+} from './ssh-status-segment-copy'
import { SshTargetStatusRow } from './SshTargetStatusRow'
import type { RemoteRuntimeSharedConnectionDiagnostics } from '../../../../shared/remote-runtime-shared-control-types'
import { connectRuntimeEnvironmentAndRecordStatus } from './runtime-environment-explicit-connect'
@@ -59,10 +64,6 @@ function overallDotColor(
}
}
-function connectedHostCountLabel(count: number): string {
- return `${count} ${count === 1 ? 'host' : 'hosts'}`
-}
-
function sshStatusForOverall(status: SshConnectionStatus): HostStatus {
if (status === 'connected') {
return 'connected'
@@ -286,9 +287,7 @@ export function SshStatusSegment({
(t) => t.syncStatus?.phase === 'conflict' || t.syncStatus?.phase === 'error'
)
const syncProblemLabel = syncProblem
- ? syncProblem.syncStatus?.phase === 'conflict'
- ? 'Workspace conflict'
- : 'Workspace sync error'
+ ? workspaceSyncProblemLabel(syncProblem.syncStatus?.phase)
: null
return (
{syncProblemLabel ??
- (anyConnecting ? 'Connecting…' : connectedHostCountLabel(connectedHostCount))}
+ (anyConnecting
+ ? connectingHostsLabel()
+ : connectedHostCountLabel(connectedHostCount))}
)}
diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx
index 9d7534e8c3b..1b3075e305b 100644
--- a/src/renderer/src/components/status-bar/StatusBar.tsx
+++ b/src/renderer/src/components/status-bar/StatusBar.tsx
@@ -2369,7 +2369,11 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
className="relative inline-flex size-5 cursor-pointer items-center justify-center rounded border border-border bg-secondary text-secondary-foreground shadow-xs transition-colors hover:bg-accent hover:text-accent-foreground"
aria-label={
showFloatingWorkspaceAttentionDot
- ? `${floatingTerminalActionLabel}, new activity`
+ ? translate(
+ 'auto.components.status.bar.StatusBar.floatingTerminalNewActivity',
+ '{{label}}, new activity',
+ { label: floatingTerminalActionLabel }
+ )
: floatingTerminalActionLabel
}
onClick={() => {
diff --git a/src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts b/src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts
index 42b42d492b6..7f6d1eefa3d 100644
--- a/src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts
+++ b/src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts
@@ -1,4 +1,12 @@
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
+
+// Mirrors resource-memory-metric-copy.test.ts: assert the English source copy
+// through the catalog fallback, with placeholders filled the way i18next would.
+vi.mock('@/i18n/i18n', () => ({
+ translate: (_key: string, fallback: string, options?: Record) =>
+ fallback.replace(/\{\{(\w+)\}\}/g, (match, name: string) => String(options?.[name] ?? match))
+}))
+
import {
formatTerminalSessionCount,
getResourceManagerAriaLabel,
@@ -19,8 +27,12 @@ describe('resource manager terminal copy', () => {
spaceScanReady: false
})
).toEqual([
- 'Resource Manager - 512 MB · Σ RSS - 2 terminal sessions',
- 'Terminal sessions are grouped by workspace.'
+ {
+ id: 'summary',
+ text: 'Resource Manager - 512 MB · Σ RSS - 2 terminal sessions',
+ emphasized: false
+ },
+ { id: 'sessions-hint', text: 'Terminal sessions are grouped by workspace.', emphasized: false }
])
})
@@ -32,12 +44,45 @@ describe('resource manager terminal copy', () => {
spaceScanReady: true
})
).toEqual([
- 'Resource Manager - memory unavailable - 0 terminal sessions',
- 'Space scan ready',
- 'No terminal sessions yet.'
+ {
+ id: 'summary',
+ text: 'Resource Manager - memory unavailable - 0 terminal sessions',
+ emphasized: false
+ },
+ { id: 'space-scan', text: 'Space scan ready', emphasized: true },
+ { id: 'sessions-hint', text: 'No terminal sessions yet.', emphasized: false }
])
})
+ // Why: the tooltip used to tint this row by matching its English text, so any
+ // translated build lost the tint. The flag is what the segment reads now.
+ it('flags the space-scan row instead of leaving callers to match its wording', () => {
+ const lines = getResourceManagerTooltipLines({
+ memoryLabel: '512 MB',
+ sessionCount: 1,
+ spaceScanReady: true
+ })
+
+ expect(lines.filter((line) => line.emphasized).map((line) => line.text)).toEqual([
+ 'Space scan ready'
+ ])
+ })
+
+ // Why: the tooltip keys rows by id, so a repeated id would silently drop a row.
+ it('gives every tooltip row a unique id to key on', () => {
+ for (const spaceScanReady of [false, true]) {
+ for (const sessionCount of [0, 1, 4]) {
+ const ids = getResourceManagerTooltipLines({
+ memoryLabel: '512 MB',
+ sessionCount,
+ spaceScanReady
+ }).map((line) => line.id)
+
+ expect(new Set(ids).size).toBe(ids.length)
+ }
+ }
+ })
+
it('keeps the trigger label descriptive for screen readers', () => {
expect(
getResourceManagerAriaLabel({
diff --git a/src/renderer/src/components/status-bar/resource-manager-terminal-copy.ts b/src/renderer/src/components/status-bar/resource-manager-terminal-copy.ts
index 69b5b7b6193..0d082654199 100644
--- a/src/renderer/src/components/status-bar/resource-manager-terminal-copy.ts
+++ b/src/renderer/src/components/status-bar/resource-manager-terminal-copy.ts
@@ -1,30 +1,86 @@
+import { translate } from '@/i18n/i18n'
+
export function formatTerminalSessionCount(count: number): string {
- return `${count} terminal session${count === 1 ? '' : 's'}`
+ return count === 1
+ ? translate(
+ 'auto.components.status.bar.resource.manager.terminal.copy.terminalSessionCount_one',
+ '{{count}} terminal session',
+ { count }
+ )
+ : translate(
+ 'auto.components.status.bar.resource.manager.terminal.copy.terminalSessionCount_other',
+ '{{count}} terminal sessions',
+ { count }
+ )
+}
+
+function spaceScanReadyLabel(): string {
+ return translate(
+ 'auto.components.status.bar.resource.manager.terminal.copy.spaceScanReady',
+ 'Space scan ready'
+ )
+}
+
+/**
+ * `emphasized` marks the space-scan row the tooltip tints. The segment used to
+ * recognize that row by comparing it against the English text, which stops
+ * matching the moment the copy comes from the catalog.
+ *
+ * `id` names the row's role. Each role appears at most once per tooltip, so it is
+ * a unique React key that survives locale switches and memory/count updates —
+ * unlike the translated text, which locales are free to duplicate across rows.
+ */
+export type ResourceManagerTooltipLine = {
+ id: 'summary' | 'space-scan' | 'sessions-hint'
+ text: string
+ emphasized: boolean
}
export function getResourceManagerTooltipLines(args: {
memoryLabel: string
sessionCount: number
spaceScanReady: boolean
-}): string[] {
+}): ResourceManagerTooltipLine[] {
const rawMemoryLabel = args.memoryLabel.trim()
const memoryLabel =
rawMemoryLabel === '' || rawMemoryLabel === '-' || rawMemoryLabel === '—'
- ? 'memory unavailable'
+ ? translate(
+ 'auto.components.status.bar.resource.manager.terminal.copy.memoryUnavailable',
+ 'memory unavailable'
+ )
: rawMemoryLabel
- const lines = [
- `Resource Manager - ${memoryLabel} - ${formatTerminalSessionCount(args.sessionCount)}`
+ // Why: whole lines are single keys — locales reorder the summary and repunctuate
+ // its separators, so it can't be concatenated from translated fragments here.
+ const lines: ResourceManagerTooltipLine[] = [
+ {
+ id: 'summary',
+ text: translate(
+ 'auto.components.status.bar.resource.manager.terminal.copy.tooltipSummary',
+ 'Resource Manager - {{memory}} - {{sessions}}',
+ { memory: memoryLabel, sessions: formatTerminalSessionCount(args.sessionCount) }
+ ),
+ emphasized: false
+ }
]
if (args.spaceScanReady) {
- lines.push('Space scan ready')
+ lines.push({ id: 'space-scan', text: spaceScanReadyLabel(), emphasized: true })
}
- if (args.sessionCount > 0) {
- lines.push('Terminal sessions are grouped by workspace.')
- } else {
- lines.push('No terminal sessions yet.')
- }
+ lines.push({
+ id: 'sessions-hint',
+ text:
+ args.sessionCount > 0
+ ? translate(
+ 'auto.components.status.bar.resource.manager.terminal.copy.sessionsGroupedByWorkspace',
+ 'Terminal sessions are grouped by workspace.'
+ )
+ : translate(
+ 'auto.components.status.bar.resource.manager.terminal.copy.noTerminalSessions',
+ 'No terminal sessions yet.'
+ ),
+ emphasized: false
+ })
return lines
}
@@ -33,11 +89,19 @@ export function getResourceManagerAriaLabel(args: {
sessionCount: number
spaceScanReady: boolean
}): string {
- const parts = ['Resource Manager', formatTerminalSessionCount(args.sessionCount)]
+ const sessions = formatTerminalSessionCount(args.sessionCount)
if (args.spaceScanReady) {
- parts.push('Space scan ready')
+ return translate(
+ 'auto.components.status.bar.resource.manager.terminal.copy.ariaLabelWithSpaceScan',
+ 'Resource Manager, {{sessions}}, {{spaceScan}}',
+ { sessions, spaceScan: spaceScanReadyLabel() }
+ )
}
- return parts.join(', ')
+ return translate(
+ 'auto.components.status.bar.resource.manager.terminal.copy.ariaLabel',
+ 'Resource Manager, {{sessions}}',
+ { sessions }
+ )
}
diff --git a/src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts b/src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts
new file mode 100644
index 00000000000..3cd3957872f
--- /dev/null
+++ b/src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts
@@ -0,0 +1,26 @@
+import { describe, expect, it, vi } from 'vitest'
+
+vi.mock('@/i18n/i18n', () => ({
+ translate: (_key: string, fallback: string, options?: Record) =>
+ fallback.replace(/\{\{(\w+)\}\}/g, (match, name: string) => String(options?.[name] ?? match))
+}))
+
+import {
+ connectedHostCountLabel,
+ connectingHostsLabel,
+ workspaceSyncProblemLabel
+} from './ssh-status-segment-copy'
+
+describe('ssh status segment copy', () => {
+ it('keeps the host noun singular for a lone connected host', () => {
+ expect(connectedHostCountLabel(0)).toBe('0 hosts')
+ expect(connectedHostCountLabel(1)).toBe('1 host')
+ expect(connectedHostCountLabel(4)).toBe('4 hosts')
+ })
+
+ it('distinguishes a sync conflict from a sync failure', () => {
+ expect(connectingHostsLabel()).toBe('Connecting…')
+ expect(workspaceSyncProblemLabel('conflict')).toBe('Workspace conflict')
+ expect(workspaceSyncProblemLabel('error')).toBe('Workspace sync error')
+ })
+})
diff --git a/src/renderer/src/components/status-bar/ssh-status-segment-copy.ts b/src/renderer/src/components/status-bar/ssh-status-segment-copy.ts
new file mode 100644
index 00000000000..cf4cd7e3552
--- /dev/null
+++ b/src/renderer/src/components/status-bar/ssh-status-segment-copy.ts
@@ -0,0 +1,31 @@
+import { translate } from '@/i18n/i18n'
+
+export function connectedHostCountLabel(count: number): string {
+ return count === 1
+ ? translate(
+ 'auto.components.status.bar.SshStatusSegment.connectedHostCount_one',
+ '{{count}} host',
+ { count }
+ )
+ : translate(
+ 'auto.components.status.bar.SshStatusSegment.connectedHostCount_other',
+ '{{count}} hosts',
+ { count }
+ )
+}
+
+export function connectingHostsLabel(): string {
+ return translate('auto.components.status.bar.SshStatusSegment.connecting', 'Connecting…')
+}
+
+export function workspaceSyncProblemLabel(phase: string | undefined): string {
+ return phase === 'conflict'
+ ? translate(
+ 'auto.components.status.bar.SshStatusSegment.workspaceConflict',
+ 'Workspace conflict'
+ )
+ : translate(
+ 'auto.components.status.bar.SshStatusSegment.workspaceSyncError',
+ 'Workspace sync error'
+ )
+}
diff --git a/src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx b/src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx
new file mode 100644
index 00000000000..96da506d9e9
--- /dev/null
+++ b/src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx
@@ -0,0 +1,183 @@
+// @vitest-environment happy-dom
+/**
+ * The Resource Manager tooltip and the SSH segment's host count were built from
+ * bare English literals inside helper functions, so they stayed English while
+ * every label around them translated. The coverage audit cannot see values
+ * returned from helpers, so only a runtime assertion against the real catalog
+ * keeps them honest — same reasoning as
+ * `src/renderer/src/i18n/settings-status-label-localization.test.ts`.
+ */
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
+import type { SshConnectionStatus } from '../../../../shared/ssh-types'
+import { i18n } from '@/i18n/i18n'
+import {
+ formatTerminalSessionCount,
+ getResourceManagerAriaLabel,
+ getResourceManagerTooltipLines
+} from './resource-manager-terminal-copy'
+import { SshStatusSegment } from './SshStatusSegment'
+
+type StoreState = {
+ sshConnectionStates: Map
+ sshTargetLabels: Map
+ remoteWorkspaceSyncStatusByTargetId: Record
+}
+
+let storeState: StoreState = {
+ sshConnectionStates: new Map(),
+ sshTargetLabels: new Map(),
+ remoteWorkspaceSyncStatusByTargetId: {}
+}
+
+vi.mock('../../store', () => {
+ const state = (): Record => ({
+ ...storeState,
+ settings: null,
+ runtimeEnvironments: [],
+ runtimeStatusByEnvironmentId: new Map(),
+ setRuntimeEnvironmentStatus: vi.fn(),
+ hydrateRuntimeEnvironmentStatuses: vi.fn(),
+ setActiveView: vi.fn(),
+ openSettingsTarget: vi.fn(),
+ recordFeatureInteraction: vi.fn(),
+ fetchRuntimeEnvironmentRepos: vi.fn(),
+ fetchWorktrees: vi.fn(),
+ fetchWorktreeLineage: vi.fn()
+ })
+ const useAppStore = (selector: (value: Record) => unknown): unknown =>
+ selector(state())
+ useAppStore.getState = state
+ return { useAppStore }
+})
+
+function setSshTargets(
+ entries: { id: string; label: string; status: SshConnectionStatus; syncPhase?: string }[]
+): void {
+ storeState = {
+ sshConnectionStates: new Map(entries.map((entry) => [entry.id, { status: entry.status }])),
+ sshTargetLabels: new Map(entries.map((entry) => [entry.id, entry.label])),
+ remoteWorkspaceSyncStatusByTargetId: Object.fromEntries(
+ entries.flatMap((entry) => (entry.syncPhase ? [[entry.id, { phase: entry.syncPhase }]] : []))
+ )
+ }
+}
+
+function triggerText(): string {
+ return screen.getByRole('button').textContent ?? ''
+}
+
+describe('status-bar copy under a non-English UI language', () => {
+ beforeAll(async () => {
+ await i18n.changeLanguage('ja')
+ })
+
+ afterEach(() => {
+ cleanup()
+ })
+
+ afterAll(async () => {
+ await i18n.changeLanguage('en')
+ })
+
+ it('translates both plural forms of the terminal session count', () => {
+ expect(formatTerminalSessionCount(1)).toBe('1 件のターミナルセッション')
+ expect(formatTerminalSessionCount(5)).toBe('5 件のターミナルセッション')
+ })
+
+ it('translates every Resource Manager tooltip line', () => {
+ expect(
+ getResourceManagerTooltipLines({
+ memoryLabel: '512 MB',
+ sessionCount: 2,
+ spaceScanReady: true
+ })
+ ).toEqual([
+ {
+ id: 'summary',
+ text: 'リソースマネージャー - 512 MB - 2 件のターミナルセッション',
+ emphasized: false
+ },
+ { id: 'space-scan', text: '容量スキャンの準備完了', emphasized: true },
+ {
+ id: 'sessions-hint',
+ text: 'ターミナルセッションはワークスペースごとにグループ化されます。',
+ emphasized: false
+ }
+ ])
+ })
+
+ // Why: the tint used to be selected by `line === 'Space scan ready'`, so it
+ // silently vanished for every non-English locale once the copy translated.
+ it('keeps the space-scan row flagged when its copy is no longer English', () => {
+ const lines = getResourceManagerTooltipLines({
+ memoryLabel: '512 MB',
+ sessionCount: 2,
+ spaceScanReady: true
+ })
+
+ const emphasized = lines.filter((line) => line.emphasized)
+ expect(emphasized).toHaveLength(1)
+ expect(emphasized[0]?.text).toBe('容量スキャンの準備完了')
+ expect(emphasized[0]?.text).not.toBe('Space scan ready')
+ })
+
+ it('translates the memory-unavailable and empty-session tooltip lines', () => {
+ expect(
+ getResourceManagerTooltipLines({ memoryLabel: '—', sessionCount: 0, spaceScanReady: false })
+ ).toEqual([
+ {
+ id: 'summary',
+ text: 'リソースマネージャー - メモリ情報を取得できません - 0 件のターミナルセッション',
+ emphasized: false
+ },
+ { id: 'sessions-hint', text: 'ターミナルセッションはまだありません。', emphasized: false }
+ ])
+ })
+
+ it('translates the Resource Manager trigger label read by screen readers', () => {
+ expect(getResourceManagerAriaLabel({ sessionCount: 1, spaceScanReady: true })).toBe(
+ 'リソースマネージャー、1 件のターミナルセッション、容量スキャンの準備完了'
+ )
+ expect(getResourceManagerAriaLabel({ sessionCount: 3, spaceScanReady: false })).toBe(
+ 'リソースマネージャー、3 件のターミナルセッション'
+ )
+ })
+
+ it('translates the connected host count next to the translated aria label', () => {
+ setSshTargets([
+ { id: 'ssh-1', label: 'builder', status: 'connected' },
+ { id: 'ssh-2', label: 'openclaw', status: 'connected' }
+ ])
+ render()
+
+ expect(screen.getByRole('button').getAttribute('aria-label')).toBe('リモートホスト接続状態')
+ expect(triggerText()).toContain('2 台のホスト')
+ })
+
+ it('translates the singular host count', () => {
+ setSshTargets([{ id: 'ssh-1', label: 'builder', status: 'connected' }])
+ render()
+
+ expect(triggerText()).toContain('1 台のホスト')
+ })
+
+ it('translates the connecting and workspace-sync states', () => {
+ setSshTargets([{ id: 'ssh-1', label: 'builder', status: 'connecting' }])
+ render()
+ expect(triggerText()).toContain('接続中…')
+ cleanup()
+
+ setSshTargets([
+ { id: 'ssh-1', label: 'builder', status: 'connected', syncPhase: 'conflict' },
+ { id: 'ssh-2', label: 'openclaw', status: 'connected', syncPhase: 'error' }
+ ])
+ render()
+ expect(triggerText()).toContain('ワークスペースの競合')
+ cleanup()
+
+ setSshTargets([{ id: 'ssh-1', label: 'builder', status: 'connected', syncPhase: 'error' }])
+ render()
+ expect(triggerText()).toContain('ワークスペースの同期エラー')
+ })
+})
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index b25a212f7a2..467ca68d5aa 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -415,7 +415,8 @@
"8d6eedf97e": "Failed to register the Orca CLI in PATH.",
"0f116999f1": "Restart your shell or add the Orca CLI directory to PATH before setup.",
"15cbedc3e3": "Install the Orca CLI before running agent skill setup.",
- "windowsPathUnknown": "Orca could not check your Windows user PATH"
+ "windowsPathUnknown": "Orca could not check your Windows user PATH",
+ "refreshCliRegistration": "Refresh CLI registration status and try again."
}
}
}
@@ -631,6 +632,9 @@
"0b01ff98d2": "Port",
"7d732521ec": "Comment"
}
+ },
+ "activation": {
+ "cannotOpenFolderWorkspace": "Cannot open folder workspace"
}
},
"folderWorkspacePathStatus": {
@@ -3262,7 +3266,12 @@
"runtime_disconnect_failed": "Disconnect failed",
"runtime_reconnecting": "Reconnecting",
"runtime_last_close_reason": "Closed: {{value0}}",
- "runtime_reconnect_attempt": "Attempt {{value0}}"
+ "runtime_reconnect_attempt": "Attempt {{value0}}",
+ "connectedHostCount_one": "{{count}} host",
+ "connectedHostCount_other": "{{count}} hosts",
+ "connecting": "Connecting…",
+ "workspaceConflict": "Workspace conflict",
+ "workspaceSyncError": "Workspace sync error"
},
"StatusBar": {
"9659e38343": "Ports",
@@ -3322,7 +3331,8 @@
"antigravityUsage": "Antigravity Usage",
"antigravityUsageDetails": "Open Antigravity usage details",
"grokUsageAria": "Open Grok usage details",
- "grokUsageMenu": "Grok Usage"
+ "grokUsageMenu": "Grok Usage",
+ "floatingTerminalNewActivity": "{{label}}, new activity"
},
"StatusBarUsageEmptyCta": {
"828c764a79": "Connect an account",
@@ -3548,6 +3558,21 @@
"workingSetDescription": "Summed working set (WS). Shared pages can appear in more than one process.",
"rssDescription": "Summed resident set size (RSS). Shared or aliased pages can appear in more than one process."
}
+ },
+ "manager": {
+ "terminal": {
+ "copy": {
+ "terminalSessionCount_one": "{{count}} terminal session",
+ "terminalSessionCount_other": "{{count}} terminal sessions",
+ "memoryUnavailable": "memory unavailable",
+ "tooltipSummary": "Resource Manager - {{memory}} - {{sessions}}",
+ "spaceScanReady": "Space scan ready",
+ "sessionsGroupedByWorkspace": "Terminal sessions are grouped by workspace.",
+ "noTerminalSessions": "No terminal sessions yet.",
+ "ariaLabel": "Resource Manager, {{sessions}}",
+ "ariaLabelWithSpaceScan": "Resource Manager, {{sessions}}, {{spaceScan}}"
+ }
+ }
}
}
}
@@ -5792,7 +5817,8 @@
"f00d6aa9b5": "WSL is not available on this machine.",
"7c776ff9d8": "wsl",
"fc0fcf72fd": "Register the WSL shell command before skill setup.",
- "windowsPathUnknown": "WSL shell command PATH could not be checked"
+ "windowsPathUnknown": "WSL shell command PATH could not be checked",
+ "refreshCliRegistration": "Refresh CLI registration status and try again."
},
"CommitMessageAiPane": {
"841ed9884a": "Used by repositories that have not customized Source Control AI.",
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index ac0f5c012bf..e60f92ba08d 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -349,7 +349,8 @@
"8d6eedf97e": "No se pudo registrar Orca CLI en PATH.",
"0f116999f1": "Reinicia tu shell o agrega el directorio de Orca CLI a PATH antes de la configuración.",
"15cbedc3e3": "Instala la CLI de Orca antes de configurar skills para agentes.",
- "windowsPathUnknown": "Orca could not check your Windows user PATH"
+ "windowsPathUnknown": "Orca could not check your Windows user PATH",
+ "refreshCliRegistration": "Actualiza el estado de registro de la CLI e inténtalo de nuevo."
}
}
}
@@ -560,6 +561,9 @@
"0b01ff98d2": "Puerto",
"7d732521ec": "Comentario"
}
+ },
+ "activation": {
+ "cannotOpenFolderWorkspace": "No se puede abrir el espacio de trabajo de carpeta"
}
},
"folderWorkspacePathStatus": {
@@ -3174,7 +3178,12 @@
"runtime_disconnect_failed": "No se pudo desconectar",
"runtime_reconnecting": "Reconectando",
"runtime_last_close_reason": "Cerrado: {{value0}}",
- "runtime_reconnect_attempt": "Intento {{value0}}"
+ "runtime_reconnect_attempt": "Intento {{value0}}",
+ "connectedHostCount_one": "{{count}} host",
+ "connectedHostCount_other": "{{count}} hosts",
+ "connecting": "Conectando…",
+ "workspaceConflict": "Conflicto del espacio de trabajo",
+ "workspaceSyncError": "Error de sincronización del espacio de trabajo"
},
"StatusBar": {
"9659e38343": "Puertos",
@@ -3234,7 +3243,8 @@
"antigravityUsage": "Uso de Antigravity",
"antigravityUsageDetails": "Abrir detalles de uso de Antigravity",
"grokUsageAria": "Abrir detalles de uso de Grok",
- "grokUsageMenu": "Uso de Grok"
+ "grokUsageMenu": "Uso de Grok",
+ "floatingTerminalNewActivity": "{{label}}, nueva actividad"
},
"StatusBarUsageEmptyCta": {
"828c764a79": "Conectar una cuenta",
@@ -3444,6 +3454,21 @@
"workingSetDescription": "Suma del conjunto de trabajo (WS). Las páginas compartidas pueden aparecer en más de un proceso.",
"rssDescription": "Suma del tamaño del conjunto residente (RSS). Las páginas compartidas o con alias pueden aparecer en más de un proceso."
}
+ },
+ "manager": {
+ "terminal": {
+ "copy": {
+ "terminalSessionCount_one": "{{count}} sesión de terminal",
+ "terminalSessionCount_other": "{{count}} sesiones de terminal",
+ "memoryUnavailable": "memoria no disponible",
+ "tooltipSummary": "Administrador de recursos - {{memory}} - {{sessions}}",
+ "spaceScanReady": "Escaneo de espacio listo",
+ "sessionsGroupedByWorkspace": "Las sesiones de terminal se agrupan por espacio de trabajo.",
+ "noTerminalSessions": "Aún no hay sesiones de terminal.",
+ "ariaLabel": "Administrador de recursos, {{sessions}}",
+ "ariaLabelWithSpaceScan": "Administrador de recursos, {{sessions}}, {{spaceScan}}"
+ }
+ }
}
},
"SkillUpdateStatusSegment": {
@@ -5624,7 +5649,8 @@
"f00d6aa9b5": "WSL no está disponible en esta máquina.",
"7c776ff9d8": "wsl",
"fc0fcf72fd": "Registra el comando de shell de WSL antes de configurar el skill.",
- "windowsPathUnknown": "WSL shell command PATH could not be checked"
+ "windowsPathUnknown": "WSL shell command PATH could not be checked",
+ "refreshCliRegistration": "Actualiza el estado de registro de la CLI e inténtalo de nuevo."
},
"CommitMessageAiPane": {
"841ed9884a": "Usado por repositorios que no han personalizado Source Control AI.",
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index d239b240df0..f008abfa70e 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -349,7 +349,8 @@
"8d6eedf97e": "Orca CLI を PATH に登録できませんでした。",
"0f116999f1": "セットアップ前にシェルを再起動するか、Orca CLI ディレクトリを PATH に追加します。",
"15cbedc3e3": "agent スキル セットアップを実行する前に、Orca CLI をインストールします。",
- "windowsPathUnknown": "Orca could not check your Windows user PATH"
+ "windowsPathUnknown": "Orca could not check your Windows user PATH",
+ "refreshCliRegistration": "CLI の登録状態を更新してから、もう一度お試しください。"
}
}
}
@@ -560,6 +561,9 @@
"0b01ff98d2": "ポート",
"7d732521ec": "コメント"
}
+ },
+ "activation": {
+ "cannotOpenFolderWorkspace": "フォルダーワークスペースを開けません"
}
},
"folderWorkspacePathStatus": {
@@ -3174,7 +3178,12 @@
"runtime_disconnect_failed": "切断に失敗しました",
"runtime_reconnecting": "再接続中",
"runtime_last_close_reason": "クローズ: {{value0}}",
- "runtime_reconnect_attempt": "試行 {{value0}}"
+ "runtime_reconnect_attempt": "試行 {{value0}}",
+ "connectedHostCount_one": "{{count}} 台のホスト",
+ "connectedHostCount_other": "{{count}} 台のホスト",
+ "connecting": "接続中…",
+ "workspaceConflict": "ワークスペースの競合",
+ "workspaceSyncError": "ワークスペースの同期エラー"
},
"StatusBar": {
"9659e38343": "ポート",
@@ -3234,7 +3243,8 @@
"antigravityUsage": "Antigravity の使用状況",
"antigravityUsageDetails": "Antigravity の使用状況詳細を開く",
"grokUsageAria": "Grok の使用状況詳細を開く",
- "grokUsageMenu": "Grok の使用状況"
+ "grokUsageMenu": "Grok の使用状況",
+ "floatingTerminalNewActivity": "{{label}}、新しいアクティビティ"
},
"StatusBarUsageEmptyCta": {
"828c764a79": "アカウントを接続する",
@@ -3444,6 +3454,21 @@
"workingSetDescription": "合計ワーキングセット(WS)。共有ページが複数のプロセスに表示されることがあります。",
"rssDescription": "合計レジデントセットサイズ(RSS)。共有またはエイリアスされたページが複数のプロセスに表示されることがあります。"
}
+ },
+ "manager": {
+ "terminal": {
+ "copy": {
+ "terminalSessionCount_one": "{{count}} 件のターミナルセッション",
+ "terminalSessionCount_other": "{{count}} 件のターミナルセッション",
+ "memoryUnavailable": "メモリ情報を取得できません",
+ "tooltipSummary": "リソースマネージャー - {{memory}} - {{sessions}}",
+ "spaceScanReady": "容量スキャンの準備完了",
+ "sessionsGroupedByWorkspace": "ターミナルセッションはワークスペースごとにグループ化されます。",
+ "noTerminalSessions": "ターミナルセッションはまだありません。",
+ "ariaLabel": "リソースマネージャー、{{sessions}}",
+ "ariaLabelWithSpaceScan": "リソースマネージャー、{{sessions}}、{{spaceScan}}"
+ }
+ }
}
},
"SkillUpdateStatusSegment": {
@@ -5609,7 +5634,8 @@
"f00d6aa9b5": "このマシンでは WSL を利用できません。",
"7c776ff9d8": "wsl",
"fc0fcf72fd": "スキルセットアップ前に WSL シェルコマンドを登録してください。",
- "windowsPathUnknown": "WSL shell command PATH could not be checked"
+ "windowsPathUnknown": "WSL shell command PATH could not be checked",
+ "refreshCliRegistration": "CLI の登録状態を更新してから、もう一度お試しください。"
},
"CommitMessageAiPane": {
"841ed9884a": "ソース管理 AI をカスタマイズしていない repos によって使用されます。",
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index 77f37c36a19..1b4c5c4fba2 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -349,7 +349,8 @@
"8d6eedf97e": "PATH에 Orca CLI를 등록하지 못했습니다.",
"0f116999f1": "설정하기 전에 셸을 다시 시작하거나 Orca CLI 디렉터리를 PATH에 추가하세요.",
"15cbedc3e3": "agent 스킬 설정을 실행하기 전에 Orca CLI를 설치하십시오.",
- "windowsPathUnknown": "Orca could not check your Windows user PATH"
+ "windowsPathUnknown": "Orca could not check your Windows user PATH",
+ "refreshCliRegistration": "CLI 등록 상태를 새로 고친 후 다시 시도하세요."
}
}
}
@@ -560,6 +561,9 @@
"0b01ff98d2": "포트",
"7d732521ec": "댓글"
}
+ },
+ "activation": {
+ "cannotOpenFolderWorkspace": "폴더 워크스페이스를 열 수 없습니다"
}
},
"folderWorkspacePathStatus": {
@@ -3174,7 +3178,12 @@
"runtime_disconnect_failed": "연결 해제 실패",
"runtime_reconnecting": "다시 연결 중",
"runtime_last_close_reason": "닫힘: {{value0}}",
- "runtime_reconnect_attempt": "{{value0}}번째 시도"
+ "runtime_reconnect_attempt": "{{value0}}번째 시도",
+ "connectedHostCount_one": "호스트 {{count}}개",
+ "connectedHostCount_other": "호스트 {{count}}개",
+ "connecting": "연결 중…",
+ "workspaceConflict": "워크스페이스 충돌",
+ "workspaceSyncError": "워크스페이스 동기화 오류"
},
"StatusBar": {
"9659e38343": "포트",
@@ -3234,7 +3243,8 @@
"antigravityUsage": "Antigravity 사용량",
"antigravityUsageDetails": "Antigravity 사용량 세부 정보 열기",
"grokUsageAria": "Grok 사용량 세부 정보 열기",
- "grokUsageMenu": "Grok 사용량"
+ "grokUsageMenu": "Grok 사용량",
+ "floatingTerminalNewActivity": "{{label}}, 새 활동"
},
"StatusBarUsageEmptyCta": {
"828c764a79": "계정 연결",
@@ -3444,6 +3454,21 @@
"workingSetDescription": "합산된 워킹 세트(WS). 공유 페이지가 둘 이상의 프로세스에 나타날 수 있습니다.",
"rssDescription": "합산된 레지던트 세트 크기(RSS). 공유 또는 별칭된 페이지가 둘 이상의 프로세스에 나타날 수 있습니다."
}
+ },
+ "manager": {
+ "terminal": {
+ "copy": {
+ "terminalSessionCount_one": "터미널 세션 {{count}}개",
+ "terminalSessionCount_other": "터미널 세션 {{count}}개",
+ "memoryUnavailable": "메모리 정보 없음",
+ "tooltipSummary": "리소스 관리자 - {{memory}} - {{sessions}}",
+ "spaceScanReady": "공간 스캔 준비됨",
+ "sessionsGroupedByWorkspace": "터미널 세션은 워크스페이스별로 그룹화됩니다.",
+ "noTerminalSessions": "아직 터미널 세션이 없습니다.",
+ "ariaLabel": "리소스 관리자, {{sessions}}",
+ "ariaLabelWithSpaceScan": "리소스 관리자, {{sessions}}, {{spaceScan}}"
+ }
+ }
}
},
"SkillUpdateStatusSegment": {
@@ -5608,8 +5633,9 @@
"0c9f3cf9da": "Orca가 글로벌 agent 스킬을 확인하고 설치하는 위치를 선택합니다.",
"f00d6aa9b5": "이 컴퓨터에서는 WSL을 사용할 수 없습니다.",
"7c776ff9d8": "wsl",
- "fc0fcf72fd": "스킬 설정 전 WSL 셸 명령어를 등록하세요.",
- "windowsPathUnknown": "WSL shell command PATH could not be checked"
+ "fc0fcf72fd": "스킬 설정 전 WSL 쉘 명령어를 등록하세요.",
+ "windowsPathUnknown": "WSL shell command PATH could not be checked",
+ "refreshCliRegistration": "CLI 등록 상태를 새로 고친 후 다시 시도하세요."
},
"CommitMessageAiPane": {
"841ed9884a": "소스 제어 AI를 사용자 정의하지 않은 리포지토리에서 사용됩니다.",
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index b71ed76c0e6..90628fb006f 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -349,7 +349,8 @@
"8d6eedf97e": "无法在 PATH 中注册 Orca CLI。",
"0f116999f1": "在安装之前重新启动 shell 或将 Orca CLI 目录添加到 PATH。",
"15cbedc3e3": "在运行智能体技能设置之前安装 Orca CLI。",
- "windowsPathUnknown": "Orca 无法检查您的 Windows 用户 PATH"
+ "windowsPathUnknown": "Orca 无法检查您的 Windows 用户 PATH",
+ "refreshCliRegistration": "请刷新 CLI 注册状态后重试。"
}
}
}
@@ -560,6 +561,9 @@
"0b01ff98d2": "端口",
"7d732521ec": "评论"
}
+ },
+ "activation": {
+ "cannotOpenFolderWorkspace": "无法打开文件夹工作区"
}
},
"folderWorkspacePathStatus": {
@@ -3186,7 +3190,12 @@
"runtime_disconnect_failed": "断开连接失败",
"runtime_reconnecting": "重新连接中",
"runtime_last_close_reason": "已关闭:{{value0}}",
- "runtime_reconnect_attempt": "第 {{value0}} 次尝试"
+ "runtime_reconnect_attempt": "第 {{value0}} 次尝试",
+ "connectedHostCount_one": "{{count}} 个主机",
+ "connectedHostCount_other": "{{count}} 个主机",
+ "connecting": "正在连接…",
+ "workspaceConflict": "工作区冲突",
+ "workspaceSyncError": "工作区同步错误"
},
"StatusBar": {
"9659e38343": "端口",
@@ -3246,7 +3255,8 @@
"antigravityUsage": "Antigravity 使用量",
"antigravityUsageDetails": "打开 Antigravity 使用详情",
"grokUsageAria": "打开 Grok 使用详情",
- "grokUsageMenu": "Grok 使用量"
+ "grokUsageMenu": "Grok 使用量",
+ "floatingTerminalNewActivity": "{{label}},有新活动"
},
"StatusBarUsageEmptyCta": {
"828c764a79": "连接账户",
@@ -3456,6 +3466,21 @@
"workingSetDescription": "工作集 (WS) 的总和。共享页可能会出现在多个进程中。",
"rssDescription": "驻留集大小 (RSS) 的总和。共享页或别名映射页可能会出现在多个进程中。"
}
+ },
+ "manager": {
+ "terminal": {
+ "copy": {
+ "terminalSessionCount_one": "{{count}} 个终端会话",
+ "terminalSessionCount_other": "{{count}} 个终端会话",
+ "memoryUnavailable": "内存信息不可用",
+ "tooltipSummary": "资源管理器 - {{memory}} - {{sessions}}",
+ "spaceScanReady": "空间扫描已就绪",
+ "sessionsGroupedByWorkspace": "终端会话按工作区分组。",
+ "noTerminalSessions": "暂无终端会话。",
+ "ariaLabel": "资源管理器,{{sessions}}",
+ "ariaLabelWithSpaceScan": "资源管理器,{{sessions}},{{spaceScan}}"
+ }
+ }
}
},
"SkillUpdateStatusSegment": {
@@ -5621,7 +5646,8 @@
"f00d6aa9b5": "WSL 在此计算机上不可用。",
"7c776ff9d8": "wsl",
"fc0fcf72fd": "在技能设置之前注册 WSL shell 命令。",
- "windowsPathUnknown": "无法检查 WSL shell 命令 PATH"
+ "windowsPathUnknown": "无法检查 WSL shell 命令 PATH",
+ "refreshCliRegistration": "请刷新 CLI 注册状态后重试。"
},
"CommitMessageAiPane": {
"841ed9884a": "由未自定义源代码控制 AI 的存储库使用。",
diff --git a/src/renderer/src/lib/agent-skill-cli-prerequisite.ts b/src/renderer/src/lib/agent-skill-cli-prerequisite.ts
index 1c731bae1fe..aa02dec7460 100644
--- a/src/renderer/src/lib/agent-skill-cli-prerequisite.ts
+++ b/src/renderer/src/lib/agent-skill-cli-prerequisite.ts
@@ -117,7 +117,14 @@ function showCliPrerequisiteWarning(status: CliInstallStatus): void {
'auto.lib.agent.skill.cli.prerequisite.windowsPathUnknown',
'Orca could not check your Windows user PATH'
),
- { description: status.detail ?? 'Refresh CLI registration status and try again.' }
+ {
+ description:
+ status.detail ??
+ translate(
+ 'auto.lib.agent.skill.cli.prerequisite.refreshCliRegistration',
+ 'Refresh CLI registration status and try again.'
+ )
+ }
)
return
}
diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts
index c5b57fd1a9d..7e317024123 100644
--- a/src/renderer/src/lib/worktree-activation.ts
+++ b/src/renderer/src/lib/worktree-activation.ts
@@ -19,6 +19,7 @@ import { buildSetupRunnerCommand } from './setup-runner'
import { createSequencedSetupAgentCommands } from '../../../shared/setup-agent-sequencing'
import { getSetupRunnerCommandPlatformForPath } from '../../../shared/setup-runner-command'
import { agentKindToTuiAgent } from '../../../shared/agent-kind'
+import { translate } from '@/i18n/i18n'
import { useAppStore } from '@/store'
import type { PendingSidebarWorktreeReveal } from '@/store/slices/ui'
import { tabHasLivePty } from '@/lib/tab-has-live-pty'
@@ -231,7 +232,13 @@ export function activateAndRevealFolderWorkspace(
{ runtimeEnvironmentId }
)
if (folderWorkspaceActivationBlocked(pathStatus)) {
- toast.error(getFolderWorkspacePathStatusTitle(pathStatus) ?? 'Cannot open folder workspace', {
+ const title =
+ getFolderWorkspacePathStatusTitle(pathStatus) ??
+ translate(
+ 'auto.lib.worktree.activation.cannotOpenFolderWorkspace',
+ 'Cannot open folder workspace'
+ )
+ toast.error(title, {
description: getFolderWorkspacePathStatusDescription(pathStatus) ?? folderWorkspace.folderPath
})
return false