Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/src/components/settings/panels/DeveloperOptionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ const developerItems = [
</svg>
),
},
{
id: 'tool-policy-diagnostics',
titleKey: 'devOptions.diagnostics',
descriptionKey: 'devOptions.toolPolicyDiagnosticsDesc',
route: 'tool-policy-diagnostics',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 17v-5a2 2 0 012-2h2a2 2 0 012 2v5m-8 0h8m-8 0H7a2 2 0 01-2-2V7a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2"
/>
</svg>
),
},
{
id: 'intelligence',
titleKey: 'settings.developerMenu.intelligence.title',
Expand Down
275 changes: 275 additions & 0 deletions app/src/components/settings/panels/ToolPolicyDiagnosticsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import { useEffect, useMemo, useState } from 'react';

import { useT } from '../../../lib/i18n/I18nContext';
import { callCoreRpc } from '../../../services/coreRpcClient';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';

type ToolPolicyDiagnostics = {
total_tools: number;
enabled_tools: number;
mcp_stdio_tools: number;
json_rpc_tools: number;
possible_write_surfaces: string[];
policy_surfaces: string[];
posture: {
autonomy_level: string;
workspace_only: boolean;
max_actions_per_hour: number;
require_approval_for_medium_risk: boolean;
block_high_risk_commands: boolean;
};
mcp_allowlists: {
enabled: boolean;
server_count: number;
enabled_server_count: number;
servers: {
name: string;
enabled: boolean;
allowed_tools_count: number;
disallowed_tools_count: number;
has_allowlist: boolean;
has_denylist: boolean;
}[];
};
mcp_write_audit: { enabled: boolean; recent_rows: number | null; last_error: string | null };
recent_denials: {
timestamp_ms: number;
tool_name: string;
policy: string;
action: string;
reason: string;
}[];
};

const ToolPolicyDiagnosticsPanel = () => {
const { t } = useT();
const { navigateBack, breadcrumbs } = useSettingsNavigation();

const [status, setStatus] = useState<
| { kind: 'loading' }
| { kind: 'ready'; diagnostics: ToolPolicyDiagnostics }
| { kind: 'error'; message: string }
>({ kind: 'loading' });

useEffect(() => {
let cancelled = false;
(async () => {
try {
const diagnostics = await callCoreRpc<ToolPolicyDiagnostics>({
method: 'tool_registry.diagnostics',
params: {},
timeoutMs: 10_000,
});
if (cancelled) return;
setStatus({ kind: 'ready', diagnostics });
} catch (err) {
if (cancelled) return;
setStatus({ kind: 'error', message: err instanceof Error ? err.message : String(err) });
}
})();
return () => {
cancelled = true;
};
}, []);

const body = useMemo(() => {
if (status.kind === 'loading') {
return (
<div className="px-4 py-3 text-sm text-sage-700 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.loading')}
</div>
);
}
if (status.kind === 'error') {
return (
<div className="px-4 py-3 rounded-lg border border-coral-300 dark:border-coral-500/40 bg-coral-50 dark:bg-coral-500/10">
<div className="text-sm font-semibold text-coral-900 dark:text-coral-200">
{t('devOptions.toolPolicyDiagnostics.unavailable')}
</div>
<div className="text-xs text-coral-800 dark:text-coral-200 mt-1 font-mono break-words">
{status.message}
</div>
</div>
);
}

const d = status.diagnostics;
const recentRows =
d.mcp_write_audit.recent_rows === null ? '—' : String(d.mcp_write_audit.recent_rows);

return (
<div className="px-4 pt-3 pb-6 flex flex-col gap-3">
<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-sage-50 dark:bg-sage-500/10">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.posture.title')}
</div>
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-xs">
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.autonomy')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">
{d.posture.autonomy_level}
</dd>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.workspaceOnly')}
</dt>
<dd className="text-sage-900 dark:text-sage-200">{String(d.posture.workspace_only)}</dd>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.maxActionsPerHour')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">
{d.posture.max_actions_per_hour}
</dd>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.approvalMediumRisk')}
</dt>
<dd className="text-sage-900 dark:text-sage-200">
{String(d.posture.require_approval_for_medium_risk)}
</dd>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.posture.blockHighRisk')}
</dt>
<dd className="text-sage-900 dark:text-sage-200">
{String(d.posture.block_high_risk_commands)}
</dd>
</dl>
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.inventory.title')}
</div>
<dl className="mt-2 grid grid-cols-2 gap-x-6 gap-y-1 text-xs">
<div>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.inventory.totalTools')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">{d.total_tools}</dd>
</div>
<div>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.inventory.enabledTools')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">{d.enabled_tools}</dd>
</div>
<div>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.inventory.mcpStdioTools')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">{d.mcp_stdio_tools}</dd>
</div>
<div>
<dt className="text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.inventory.jsonRpcTools')}
</dt>
<dd className="font-mono text-sage-900 dark:text-sage-200">{d.json_rpc_tools}</dd>
</div>
</dl>
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.mcpAllowlists.title')}
</div>
<div className="mt-1 text-xs text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.mcpAllowlists.summary')
.replace('{enabled}', String(d.mcp_allowlists.enabled))
.replace('{enabledCount}', String(d.mcp_allowlists.enabled_server_count))
.replace('{totalCount}', String(d.mcp_allowlists.server_count))}
</div>
{d.mcp_allowlists.servers.length > 0 && (
<ul className="mt-2 text-xs space-y-1">
{d.mcp_allowlists.servers.slice(0, 10).map(s => (
<li key={s.name} className="flex items-center justify-between gap-3">
<span
className="font-mono text-sage-900 dark:text-sage-200 truncate"
title={s.name}>
{s.name || t('devOptions.toolPolicyDiagnostics.mcpAllowlists.unnamed')}
</span>
<span className="text-sage-700 dark:text-sage-300 font-mono">
{t('devOptions.toolPolicyDiagnostics.mcpAllowlists.allowDeny')
.replace('{allowCount}', String(s.allowed_tools_count))
.replace('{denyCount}', String(s.disallowed_tools_count))}
</span>
</li>
))}
</ul>
)}
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.mcpWriteAudit.title')}
</div>
<div className="mt-1 text-xs text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.mcpWriteAudit.summary')
.replace('{enabled}', String(d.mcp_write_audit.enabled))
.replace('{recentRows}', recentRows)}
</div>
{d.mcp_write_audit.last_error && (
<div className="mt-2 text-xs text-coral-700 dark:text-coral-200 font-mono break-words">
{d.mcp_write_audit.last_error}
</div>
)}
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.recentBlocked.title')}
</div>
{d.recent_denials.length === 0 ? (
<div className="mt-1 text-xs text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.recentBlocked.empty')}
</div>
) : (
<ul className="mt-2 text-xs space-y-1">
{d.recent_denials.slice(0, 10).map(entry => (
<li
key={`${entry.timestamp_ms}:${entry.tool_name}`}
className="flex flex-col gap-0.5">
<div className="flex items-center justify-between gap-3">
<span
className="font-mono text-sage-900 dark:text-sage-200 truncate"
title={entry.tool_name}>
{entry.tool_name}
</span>
<span className="text-sage-700 dark:text-sage-300 font-mono">
{entry.policy}:{entry.action}
</span>
</div>
<div className="text-sage-700 dark:text-sage-300 break-words">{entry.reason}</div>
</li>
))}
</ul>
)}
</div>

<div className="px-4 py-3 rounded-lg border border-sage-300 dark:border-sage-500/40 bg-white dark:bg-sage-900/20">
<div className="text-sm font-semibold text-sage-900 dark:text-sage-200">
{t('devOptions.toolPolicyDiagnostics.redactedSurfaces.title')}
</div>
<div className="mt-1 text-xs text-sage-700 dark:text-sage-300">
{t('devOptions.toolPolicyDiagnostics.redactedSurfaces.summary')
.replace('{writeCount}', String(d.possible_write_surfaces.length))
.replace('{policyCount}', String(d.policy_surfaces.length))}
</div>
</div>
</div>
);
}, [status, t]);

return (
<div className="z-10 relative">
<SettingsHeader
title={t('devOptions.diagnostics')}
showBackButton={true}
onBack={navigateBack}
breadcrumbs={breadcrumbs}
/>
{body}
</div>
);
};

export default ToolPolicyDiagnosticsPanel;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { screen, waitFor } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';

import { renderWithProviders } from '../../../../test/test-utils';

const hoisted = vi.hoisted(() => ({ callCoreRpc: vi.fn() }));

vi.mock('../../../../services/coreRpcClient', () => ({
callCoreRpc: (...args: unknown[]) => hoisted.callCoreRpc(...args),
}));

vi.mock('../../hooks/useSettingsNavigation', () => ({
useSettingsNavigation: () => ({ navigateBack: vi.fn(), breadcrumbs: [] }),
}));

describe('ToolPolicyDiagnosticsPanel', () => {
test('renders diagnostics from core RPC', async () => {
hoisted.callCoreRpc.mockResolvedValue({
total_tools: 10,
enabled_tools: 10,
mcp_stdio_tools: 3,
json_rpc_tools: 7,
possible_write_surfaces: ['tools.composio_execute'],
policy_surfaces: ['security.policy_info'],
posture: {
autonomy_level: 'supervised',
workspace_only: true,
max_actions_per_hour: 123,
require_approval_for_medium_risk: true,
block_high_risk_commands: true,
},
mcp_allowlists: { enabled: true, server_count: 0, enabled_server_count: 0, servers: [] },
mcp_write_audit: { enabled: true, recent_rows: 5, last_error: null },
recent_denials: [],
});

const Panel = (await import('../ToolPolicyDiagnosticsPanel')).default;
renderWithProviders(<Panel />);

await waitFor(() => {
expect(screen.getByText(/Policy posture/i)).toBeInTheDocument();
});
expect(screen.getByText('supervised')).toBeInTheDocument();
expect(screen.getByText(/Total tools/i)).toBeInTheDocument();
expect(screen.getAllByText('10').length).toBeGreaterThan(0);
expect(screen.getByText(/Recent \(24h\): 5/i)).toBeInTheDocument();
expect(hoisted.callCoreRpc).toHaveBeenCalled();
});
});
28 changes: 28 additions & 0 deletions app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,34 @@ const en: TranslationMap = {
'devOptions.title': 'Advanced',
'devOptions.diagnostics': 'Diagnostics',
'devOptions.diagnosticsDesc': 'System health, logs, and performance metrics',
'devOptions.toolPolicyDiagnosticsDesc':
'Tool inventory, policy posture, MCP allowlists, and recent blocks',
'devOptions.toolPolicyDiagnostics.loading': 'Loading…',
'devOptions.toolPolicyDiagnostics.unavailable': 'Diagnostics unavailable',
'devOptions.toolPolicyDiagnostics.posture.title': 'Policy posture',
'devOptions.toolPolicyDiagnostics.posture.autonomy': 'Autonomy:',
'devOptions.toolPolicyDiagnostics.posture.workspaceOnly': 'Workspace only:',
'devOptions.toolPolicyDiagnostics.posture.maxActionsPerHour': 'Max actions/hr:',
'devOptions.toolPolicyDiagnostics.posture.approvalMediumRisk': 'Approval (medium risk):',
'devOptions.toolPolicyDiagnostics.posture.blockHighRisk': 'Block high risk:',
'devOptions.toolPolicyDiagnostics.inventory.title': 'Inventory',
'devOptions.toolPolicyDiagnostics.inventory.totalTools': 'Total tools',
'devOptions.toolPolicyDiagnostics.inventory.enabledTools': 'Enabled tools',
'devOptions.toolPolicyDiagnostics.inventory.mcpStdioTools': 'MCP stdio tools',
'devOptions.toolPolicyDiagnostics.inventory.jsonRpcTools': 'JSON-RPC tools',
'devOptions.toolPolicyDiagnostics.mcpAllowlists.title': 'MCP allowlists',
'devOptions.toolPolicyDiagnostics.mcpAllowlists.summary':
'Enabled: {enabled} · Servers: {enabledCount}/{totalCount}',
'devOptions.toolPolicyDiagnostics.mcpAllowlists.unnamed': '<unnamed>',
'devOptions.toolPolicyDiagnostics.mcpAllowlists.allowDeny': 'allow={allowCount} deny={denyCount}',
'devOptions.toolPolicyDiagnostics.mcpWriteAudit.title': 'MCP write audit',
'devOptions.toolPolicyDiagnostics.mcpWriteAudit.summary':
'Enabled: {enabled} · Recent (24h): {recentRows}',
'devOptions.toolPolicyDiagnostics.recentBlocked.title': 'Recent blocked calls',
'devOptions.toolPolicyDiagnostics.recentBlocked.empty': 'No blocked calls recorded.',
'devOptions.toolPolicyDiagnostics.redactedSurfaces.title': 'Redacted surfaces',
'devOptions.toolPolicyDiagnostics.redactedSurfaces.summary':
'Write-capable: {writeCount} · Policy surfaces: {policyCount}',
Comment on lines +516 to +543
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking i18n parity for devOptions.toolPolicyDiagnostics.* keys"

# 1) Collect new key set from en.ts
EN_FILE="app/src/lib/i18n/en.ts"
if [[ ! -f "$EN_FILE" ]]; then
  echo "Missing $EN_FILE"
  exit 1
fi

keys=$(rg -n "'devOptions\.toolPolicyDiagnostics[^']*'" "$EN_FILE" \
  | sed -E "s/.*'([^']+)'.*/\1/" \
  | sort -u)

if [[ -z "${keys}" ]]; then
  echo "No toolPolicyDiagnostics keys found in en.ts"
  exit 0
fi

echo "Found keys:"
echo "$keys"
echo

# 2) Find locale/chunk files
mapfile -t locale_files < <(fd -p 'app/src/lib/i18n' -t f '^(en|ar|bn|de|es|fr|hi|id|it|ko|pt|ru|zh-CN)(-[0-9]+)?\.ts$' | sort)

if [[ ${`#locale_files`[@]} -eq 0 ]]; then
  echo "No locale files found under app/src/lib/i18n"
  exit 1
fi

missing_any=0
for f in "${locale_files[@]}"; do
  missing_keys=()
  while IFS= read -r k; do
    if ! rg -n "'$k'" "$f" >/dev/null; then
      missing_keys+=("$k")
    fi
  done <<< "$keys"

  if [[ ${`#missing_keys`[@]} -gt 0 ]]; then
    missing_any=1
    echo "MISSING in $f:"
    printf '  - %s\n' "${missing_keys[@]}"
    echo
  fi
done

if [[ $missing_any -eq 0 ]]; then
  echo "✅ All locale/chunk files contain the new keys."
else
  echo "❌ Some locale/chunk files are missing keys."
  exit 2
fi

Repository: tinyhumansai/openhuman

Length of output: 766


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking i18n parity for devOptions.toolPolicyDiagnostics.* keys"

EN_FILE="app/src/lib/i18n/en.ts"
if [[ ! -f "$EN_FILE" ]]; then
  echo "Missing $EN_FILE"
  exit 1
fi

# Collect all devOptions.toolPolicyDiagnostics.* keys from en.ts
keys_file="$(mktemp)"
rg -n "'devOptions\.toolPolicyDiagnostics[^']*'" "$EN_FILE" \
  | sed -E "s/.*'([^']+)'.*/\1/" \
  | sort -u > "$keys_file"

if [[ ! -s "$keys_file" ]]; then
  echo "No toolPolicyDiagnostics keys found in en.ts"
  exit 0
fi

echo "Key count: $(wc -l < "$keys_file")"

# Find locale/chunk files without process substitution
# (process substitution previously failed with /dev/fd issues in this environment)
locale_list_file="$(mktemp)"
fd -p 'app/src/lib/i18n' -t f '^(en|ar|bn|de|es|fr|hi|id|it|ko|pt|ru|zh-CN)(-[0-9]+)?\.ts$' \
  | sort > "$locale_list_file"

mapfile -t locale_files < "$locale_list_file"
if [[ ${`#locale_files`[@]} -eq 0 ]]; then
  echo "No locale files found under app/src/lib/i18n"
  exit 1
fi

missing_any=0
missing_report_file="$(mktemp)"

while IFS= read -r f; do
  missing_keys_file="$(mktemp)"
  while IFS= read -r k; do
    if ! rg -n "'$k'" "$f" >/dev/null; then
      echo "$k" >> "$missing_keys_file"
    fi
  done < "$keys_file"

  if [[ -s "$missing_keys_file" ]]; then
    missing_any=1
    {
      echo "MISSING in $f:"
      sed 's/^/  - /' "$missing_keys_file"
      echo
    } >> "$missing_report_file"
  fi

  rm -f "$missing_keys_file"
done < "$locale_list_file"

if [[ $missing_any -eq 0 ]]; then
  echo "✅ All locale/chunk files contain the new keys."
else
  echo "❌ Some locale/chunk files are missing keys:"
  cat "$missing_report_file"
  exit 2
fi

Repository: tinyhumansai/openhuman

Length of output: 310


🏁 Script executed:

#!/bin/bash
set -euo pipefail

EN_FILE="app/src/lib/i18n/en.ts"
if [[ ! -f "$EN_FILE" ]]; then
  echo "Missing $EN_FILE" >&2
  exit 1
fi

# Collect new keys from en.ts
keys_file="$(mktemp)"
rg -n "'devOptions\.toolPolicyDiagnostics[^']*'" "$EN_FILE" \
  | sed -E "s/.*'([^']+)'.*/\1/" \
  | sort -u > "$keys_file"

echo "Key count: $(wc -l < "$keys_file")"

# Enumerate locale/chunk files using find (no fd path flag ambiguity)
# Targets:
# - en.ts
# - en-<N>.ts
# - ar.ts, ar-<N>.ts, bn.ts, bn-<N>.ts, ..., zh-CN.ts, zh-CN-<N>.ts
locale_files_file="$(mktemp)"
find app/src/lib/i18n -type f \( \
  -name 'en.ts' -o -name 'en-*.ts' \
  -o -name 'ar.ts' -o -name 'ar-*.ts' \
  -o -name 'bn.ts' -o -name 'bn-*.ts' \
  -o -name 'de.ts' -o -name 'de-*.ts' \
  -o -name 'es.ts' -o -name 'es-*.ts' \
  -o -name 'fr.ts' -o -name 'fr-*.ts' \
  -o -name 'hi.ts' -o -name 'hi-*.ts' \
  -o -name 'id.ts' -o -name 'id-*.ts' \
  -o -name 'it.ts' -o -name 'it-*.ts' \
  -o -name 'ko.ts' -o -name 'ko-*.ts' \
  -o -name 'pt.ts' -o -name 'pt-*.ts' \
  -o -name 'ru.ts' -o -name 'ru-*.ts' \
  -o -name 'zh-CN.ts' -o -name 'zh-CN-*.ts' \
\) | sort > "$locale_files_file"

mapfile -t locale_files < "$locale_files_file"
echo "Locale files found: ${`#locale_files`[@]}"

missing_any=0
missing_report_file="$(mktemp)"

for f in "${locale_files[@]}"; do
  missing_keys_file="$(mktemp)"
  while IFS= read -r k; do
    if ! rg -n "'$k'" "$f" >/dev/null; then
      echo "$k" >> "$missing_keys_file"
    fi
  done < "$keys_file"

  if [[ -s "$missing_keys_file" ]]; then
    missing_any=1
    {
      echo "MISSING in $f:"
      sed 's/^/  - /' "$missing_keys_file"
      echo
    } >> "$missing_report_file"
  fi
  rm -f "$missing_keys_file"
done

if [[ $missing_any -eq 0 ]]; then
  echo "✅ All locale/chunk files contain the new keys."
else
  echo "❌ Some locale/chunk files are missing keys:"
  cat "$missing_report_file"
  exit 2
fi

Repository: tinyhumansai/openhuman

Length of output: 60380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

EN_FILE="app/src/lib/i18n/en.ts"
keys_file="$(mktemp)"

# Extract ONLY i18n key names (the quoted object keys) that end with a colon.
# Example lines supported:
#   '...': 'Some value'
#   '...':
#       'Some value',
rg -n "'devOptions\.toolPolicyDiagnostics[^']*'\s*:" "$EN_FILE" \
  | sed -E "s/.*'([^']+)'.*/\1/" \
  | sort -u > "$keys_file"

echo "devOptions.toolPolicyDiagnostics key count: $(wc -l < "$keys_file")"

# Enumerate locale/chunk files under app/src/lib/i18n
locale_files_file="$(mktemp)"
find app/src/lib/i18n -type f \( \
  -name 'en.ts' -o -name 'en-*.ts' \
  -o -name 'ar.ts' -o -name 'ar-*.ts' \
  -o -name 'bn.ts' -o -name 'bn-*.ts' \
  -o -name 'de.ts' -o -name 'de-*.ts' \
  -o -name 'es.ts' -o -name 'es-*.ts' \
  -o -name 'fr.ts' -o -name 'fr-*.ts' \
  -o -name 'hi.ts' -o -name 'hi-*.ts' \
  -o -name 'id.ts' -o -name 'id-*.ts' \
  -o -name 'it.ts' -o -name 'it-*.ts' \
  -o -name 'ko.ts' -o -name 'ko-*.ts' \
  -o -name 'pt.ts' -o -name 'pt-*.ts' \
  -o -name 'ru.ts' -o -name 'ru-*.ts' \
  -o -name 'zh-CN.ts' -o -name 'zh-CN-*.ts' \
\) | sort > "$locale_files_file"

mapfile -t locale_files < "$locale_files_file"
echo "Locale/chunk files found: ${`#locale_files`[@]}"

missing_report_file="$(mktemp)"
missing_any=0

# Compare by presence of the exact key token "'<key>'"
while IFS= read -r f; do
  missing_keys_file="$(mktemp)"
  while IFS= read -r k; do
    token="'$k'"
    if ! rg -nF "$token" "$f" >/dev/null; then
      echo "$k" >> "$missing_keys_file"
    fi
  done < "$keys_file"

  if [[ -s "$missing_keys_file" ]]; then
    missing_any=1
    {
      echo "MISSING in $f:"
      # keep output bounded
      head -n 40 "$missing_keys_file"
      if [[ $(wc -l < "$missing_keys_file") -gt 40 ]]; then
        echo "  ... (and $(( $(wc -l < "$missing_keys_file") - 40 )) more)"
      fi
      echo
    } >> "$missing_report_file"
  fi

  rm -f "$missing_keys_file"
done < "$locale_files_file"

if [[ $missing_any -eq 0 ]]; then
  echo "✅ All locale/chunk files contain every new devOptions.toolPolicyDiagnostics key."
else
  echo "❌ Missing keys detected (showing up to 40 per file):"
  cat "$missing_report_file"
  exit 2
fi

Repository: tinyhumansai/openhuman

Length of output: 45495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

EN_FILE="app/src/lib/i18n/en.ts"
CHUNKS_DIR="app/src/lib/i18n/chunks"

locales=(ar bn de es fr hi id it ko pt ru zh-CN)

# Extract all i18n keys under devOptions.toolPolicyDiagnostics... from en.ts
keys_file="$(mktemp)"
rg -o "'devOptions\.toolPolicyDiagnostics[^']*'" "$EN_FILE" \
  | sed "s/^'//; s/'$//" \
  | sort -u > "$keys_file"

echo "devOptions.toolPolicyDiagnostics key count: $(wc -l < "$keys_file")"

# English chunk files
mapfile -t en_chunk_files < <(find "$CHUNKS_DIR" -type f -name 'en-*.ts' | sort)

missing_total=0
missing_report_file="$(mktemp)"

for key in $(cat "$keys_file"); do
  # Find which en-N.ts contains this key
  matches=()
  for f in "${en_chunk_files[@]}"; do
    if rg -nF "'$key'" "$f" >/dev/null; then
      matches+=("$f")
    fi
  done

  if [[ ${`#matches`[@]} -ne 1 ]]; then
    echo "KEY '$key' appears in ${`#matches`[@]} English chunk files: ${matches[*]:-<none>}" >> "$missing_report_file"
    missing_total=1
    continue
  fi

  en_chunk="${matches[0]}"
  base="$(basename "$en_chunk")" # en-<N>.ts
  N="$(sed -E 's/^en-([0-9]+)\.ts$/\1/' <<< "$base")"

  for loc in "${locales[@]}"; do
    loc_chunk="$CHUNKS_DIR/${loc}-${N}.ts"
    if [[ ! -f "$loc_chunk" ]]; then
      echo "MISSING file: $loc_chunk (for key '$key')" >> "$missing_report_file"
      missing_total=1
      continue
    fi
    if ! rg -nF "'$key'" "$loc_chunk" >/dev/null; then
      echo "MISSING key '$key' in $loc_chunk" >> "$missing_report_file"
      missing_total=1
    fi
  done
done

if [[ $missing_total -eq 0 ]]; then
  echo "✅ All devOptions.toolPolicyDiagnostics keys are present in the corresponding en-N.ts and in every locale's ${loc}-N.ts chunk."
else
  echo "❌ Missing i18n chunk parity:"
  # limit output
  head -n 120 "$missing_report_file"
  echo "... (see full report in $missing_report_file)"
  exit 2
fi

Repository: tinyhumansai/openhuman

Length of output: 189


🏁 Script executed:

#!/bin/bash
set -euo pipefail

EN_FILE="app/src/lib/i18n/en.ts"
CHUNKS_DIR="app/src/lib/i18n/chunks"
locales=(ar bn de es fr hi id it ko pt ru zh-CN)

# Extract devOptions.toolPolicyDiagnostics keys from en.ts:
# Capture the quoted key BEFORE the colon (not the value).
keys_file="$(mktemp)"
rg -n "'devOptions\.toolPolicyDiagnostics[^']*'\s*:" "$EN_FILE" \
  | sed -E "s/^.*'([^']+)'\s*:\s*.*$/\1/" \
  | sort -u > "$keys_file"

echo "devOptions.toolPolicyDiagnostics key count: $(wc -l < "$keys_file")"

# List English chunk files (en-<N>.ts)
en_chunk_files_file="$(mktemp)"
find "$CHUNKS_DIR" -type f -name 'en-*.ts' | sort > "$en_chunk_files_file"
mapfile -t en_chunk_files < "$en_chunk_files_file"
echo "English chunk files: ${`#en_chunk_files`[@]}"

missing_any=0
missing_report_file="$(mktemp)"

while IFS= read -r key; do
  # Find the en-<N>.ts that contains this key
  matches_file="$(mktemp)"
  for f in "${en_chunk_files[@]}"; do
    if rg -nF "'$key'" "$f" >/dev/null; then
      echo "$f" >> "$matches_file"
    fi
  done
  matches_count=$(wc -l < "$matches_file")

  if [[ $matches_count -ne 1 ]]; then
    missing_any=1
    {
      echo "KEY present in $matches_count English chunk files:"
      sed 's/^/  - /' "$matches_file"
      echo "  key: $key"
      echo
    } >> "$missing_report_file"
    rm -f "$matches_file"
    continue
  fi

  en_chunk="$(cat "$matches_file")"
  rm -f "$matches_file"

  base="$(basename "$en_chunk")" # en-<N>.ts
  N="$(sed -E 's/^en-([0-9]+)\.ts$/\1/' <<< "$base")"

  for loc in "${locales[@]}"; do
    loc_chunk="$CHUNKS_DIR/${loc}-${N}.ts"
    if [[ ! -f "$loc_chunk" ]]; then
      missing_any=1
      echo "MISSING file: $loc_chunk (for key $key)" >> "$missing_report_file"
      continue
    fi
    if ! rg -nF "'$key'" "$loc_chunk" >/dev/null; then
      missing_any=1
      echo "MISSING key '$key' in $loc_chunk" >> "$missing_report_file"
    fi
  done
done < "$keys_file"

if [[ $missing_any -eq 0 ]]; then
  echo "✅ All devOptions.toolPolicyDiagnostics keys exist in corresponding en-<N>.ts and every ${locales[*]}-<N>.ts"
else
  echo "❌ Missing i18n chunk parity:"
  head -n 80 "$missing_report_file"
  echo "... (see full report in $missing_report_file)"
  exit 2
fi

Repository: tinyhumansai/openhuman

Length of output: 2583


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "devOptions\.toolPolicyDiagnostics\.inventory\.title" app/src/lib/i18n/chunks/en-*.ts || true
rg -n "devOptions\.toolPolicyDiagnostics\.mcpAllowlists\.summary" app/src/lib/i18n/chunks/en-*.ts || true
rg -n "devOptions\.toolPolicyDiagnostics\.inventory\.title" app/src/lib/i18n/chunks/ar-*.ts || true

# Also check en.ts for sanity
rg -n "devOptions\.toolPolicyDiagnostics\.inventory\.title" app/src/lib/i18n/en.ts || true

Repository: tinyhumansai/openhuman

Length of output: 137


🏁 Script executed:

#!/bin/bash
set -euo pipefail

keys=(
  "devOptions.toolPolicyDiagnostics.inventory.title"
  "devOptions.toolPolicyDiagnostics.loading"
  "devOptions.toolPolicyDiagnostics.mcpAllowlists.summary"
  "devOptions.toolPolicyDiagnostics.redactedSurfaces.title"
)

for k in "${keys[@]}"; do
  echo "---- $k in en chunks"
  rg -n "$k" app/src/lib/i18n/chunks/en-*.ts || true
done

echo "---- sanity check: same keys in ar chunks"
for k in "${keys[@]}"; do
  rg -n "$k" app/src/lib/i18n/chunks/ar-*.ts || true
done

Repository: tinyhumansai/openhuman

Length of output: 381


Propagate the new devOptions.toolPolicyDiagnostics.* i18n keys into all chunked locale files

The keys added in app/src/lib/i18n/en.ts (devOptions.toolPolicyDiagnostics.*) are missing from app/src/lib/i18n/chunks/en-*.ts and corresponding non-English chunk files (e.g., ar-*.ts), which will cause missing UI text for chunked locales. Add these exact keys to the matching en-N.ts chunk and to every non-English locale chunk (ar, bn, de, es, fr, hi, id, it, ko, pt, ru, zh-CN) using the English value as the placeholder if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/i18n/en.ts` around lines 516 - 543, The new i18n keys under
devOptions.toolPolicyDiagnostics (e.g.,
devOptions.toolPolicyDiagnostics.posture.autonomy, .inventory.totalTools,
.mcpAllowlists.summary, etc.) were added to app/src/lib/i18n/en.ts but not
propagated to the chunked locale files; update the matching en chunk file
(en-*.ts) and every non-English locale chunk (ar, bn, de, es, fr, hi, id, it,
ko, pt, ru, zh-CN) by adding the exact same key names and using the English
strings as placeholders so the UI won’t show missing text for chunked locales —
search for the devOptions.toolPolicyDiagnostics prefix to locate where to insert
the new keys in each chunk file.

'devOptions.debugPanels': 'Debug Panels',
'devOptions.debugPanelsDesc': 'Feature flags, state inspection, and debugging tools',
'devOptions.webhooks': 'Webhooks',
Expand Down
Loading
Loading