Skip to content

feat: show and sync complete Skills inventories#349

Open
LceAn wants to merge 1 commit into
mm7894215:mainfrom
LceAn:fix/skills-inventory-sync-i18n
Open

feat: show and sync complete Skills inventories#349
LceAn wants to merge 1 commit into
mm7894215:mainfrom
LceAn:fix/skills-inventory-sync-i18n

Conversation

@LceAn

@LceAn LceAn commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • scan Skills from every supported local tool, including Codex built-ins and Codex/Claude/ZCode plugin caches
  • surface built-in, plugin-managed, and other-device Skills as read-only inventory rows
  • publish a privacy-safe per-device metadata inventory and merge other-device results into the Skills page
  • localize the remaining Skills actions and states in Simplified/Traditional Chinese, Japanese, Korean, and German
  • make the Vite local Skills endpoint use the current checkout so desktop services on port 7680 cannot mask changes during development

Privacy and safety

Only stable keys, display names, relative directories, tool IDs, scope flags, and device metadata are synced. SKILL.md content, descriptions, prompts, README URLs, target paths, and absolute paths are excluded. Both the browser projection and Edge function reject absolute/path-bearing identifiers. Remote, built-in, and plugin-managed rows cannot be deleted, batch-selected, or retargeted.

Deployment required

  • apply migrations/20260721113000_add-device-skill-inventories.sql
  • deploy the tokentracker-account-skills Edge function from dashboard/edge-patches/tokentracker-account-skills.ts

Cross-device inventory respects the existing cloud-sync preference and degrades to the local inventory if auth, network, or backend sync is unavailable.

Validation

  • Dashboard Vitest: 76 files, 434 tests passed
  • Skills-focused Node tests: 48 passed, 6 platform skips, 0 failed
  • focused inventory/UI tests rerun before push: 25 passed
  • validate:copy, validate:ui-hardcode, and validate:guardrails
  • Dashboard typecheck, lint, and production build
  • git diff --check
  • local browser verification: 76/76 Skills detected; read-only states and localized controls verified

The full root npm run ci:local does not run cleanly on this Windows host because of existing environment-specific failures (missing sqlite3, symlink EPERM, and path/locale assumptions); the scoped Node suites and the complete Dashboard suite pass.

Summary by CodeRabbit

  • New Features

    • Added cross-device Skills inventory, including remote and read-only skill visibility.
    • Added inventory syncing and publishing for supported devices.
    • Added support for additional skill targets and system/plugin skill discovery.
    • Added device sources, inventory badges, usage details, and expanded Skills management UI.
    • Added localized Skills interface text for German, Japanese, Korean, Simplified Chinese, and Traditional Chinese.
  • Bug Fixes

    • Prevented read-only skills from appearing as removable unused skills or being modified through bulk actions.
    • Restricted OpenClaw inventory scanning to the active workspace.
  • Tests

    • Added coverage for inventory merging, privacy safeguards, read-only behavior, localization, and target discovery.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds per-device Skills inventory discovery, authenticated cloud storage and retrieval, cross-device merging, read-only UI handling, localized strings, and guardrail tests.

Changes

Skills inventory synchronization

Layer / File(s) Summary
Local inventory discovery
src/lib/skills-manager.js, src/lib/local-api.js, test/skills-manager-extras.test.js
Adds targets and constrained directory resolution, discovers system/plugin inventory skills, marks non-manageable targets, and excludes read-only skills from unused results.
Cloud inventory contract
migrations/..., dashboard/edge-patches/..., dashboard/src/lib/skills-api.ts, dashboard/vite.config.js, test/skills-cloud-guardrails.test.js
Adds device-scoped inventory persistence, authenticated sanitized GET/POST handling, cloud API wrappers, local endpoint routing, and security/i18n guardrails.
Inventory metadata and merging
dashboard/src/lib/skills-inventory.ts, dashboard/src/lib/skills-inventory.test.ts, dashboard/src/pages/SkillsPage.jsx
Sanitizes cloud metadata, merges local and other-device skills, and enriches installed skills while preventing stale cloud responses from replacing newer state.
Read-only Skills UI
dashboard/src/pages/SkillsPage.jsx, dashboard/src/pages/SkillDetailPanel.jsx, dashboard/src/content/i18n/*/core.json, dashboard/src/pages/SkillsPage.test.jsx
Displays inventory sources and devices, disables unsupported mutations, updates detail-panel sections, adds localized strings, and tests inventory-only read-only rendering.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SkillsPage
  participant SkillsAPI
  participant EdgeFunction
  participant InventoryDatabase
  SkillsPage->>SkillsAPI: request cloud inventory
  SkillsAPI->>EdgeFunction: authenticated GET request
  EdgeFunction->>InventoryDatabase: query active devices and inventories
  InventoryDatabase-->>EdgeFunction: device inventory rows
  EdgeFunction-->>SkillsAPI: devices payload
  SkillsAPI-->>SkillsPage: cloud inventory
  SkillsPage->>SkillsPage: merge local and cloud skills
Loading

Possibly related PRs

Suggested labels: dashboard, tests

Suggested reviewers: mm7894215

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: showing and syncing complete Skills inventories.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
dashboard/src/content/i18n/de/core.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

dashboard/src/content/i18n/ja/core.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

dashboard/src/content/i18n/ko/core.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

  • 2 others
🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dashboard/src/pages/SkillsPage.jsx (1)

1132-1140: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Use stable identity skill.id || skill.directory for selection and busy states.

Unmanaged local skills lack an id and rely on skill.directory for identity. Using skill.id exclusively for Set membership causes all unmanaged skills to map to undefined. Consequently, selecting one unmanaged skill visually selects all of them, and executing a bulk action (like remove) will unintentionally delete all unmanaged skills. Additionally, busy spinners will cross-contaminate across unmanaged rows.

  • dashboard/src/pages/SkillsPage.jsx#L1132-L1140: Use const id = skill.id || skill.directory; and add/delete id from the next set.
  • dashboard/src/pages/SkillsPage.jsx#L188-L188: Change to busy={busyKey === targetBusyKey(skill.id || skill.directory, target.id)}.
  • dashboard/src/pages/SkillsPage.jsx#L557-L558: Change to checked={selectedIds.has(skill.id || skill.directory)}.
  • dashboard/src/pages/SkillsPage.jsx#L1107-L1109: Change to runMutation(targetBusyKey(skill.id || skill.directory, targetId)...).
  • dashboard/src/pages/SkillsPage.jsx#L1146-L1148: Change filter to selectedIds.has(s.id || s.directory).
  • dashboard/src/pages/SkillsPage.jsx#L1168-L1170: Change filter to selectedIds.has(s.id || s.directory).
  • dashboard/src/pages/SkillDetailPanel.jsx#L329-L329: Change to busy={busyKey === targetBusyKey(skill.id || skill.directory, target.id)}.
🤖 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 `@dashboard/src/pages/SkillsPage.jsx` around lines 1132 - 1140, Use the stable
identity skill.id || skill.directory throughout selection and busy-state
handling. In dashboard/src/pages/SkillsPage.jsx at lines 1132-1140, 188,
557-558, 1107-1109, 1146-1148, and 1168-1170, update handleToggleSelect, checked
state, targetBusyKey calls, and selectedIds filters to use that fallback
identity; make the corresponding busy-state update in
dashboard/src/pages/SkillDetailPanel.jsx at line 329.
🧹 Nitpick comments (1)
dashboard/src/lib/skills-api.ts (1)

131-136: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a request timeout for the cloud fetch.

fetchCloudSkillsJson has no AbortController/timeout, so a stalled cloud endpoint can leave publishSkillInventory/getAccountSkillInventories (and the Skills UI busy state) hanging indefinitely. A bounded timeout would degrade gracefully to local inventory, matching the fallback intent described in the PR.

🤖 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 `@dashboard/src/lib/skills-api.ts` around lines 131 - 136, Update
fetchCloudSkillsJson to use an AbortController with a bounded timeout when
issuing the cloud fetch, ensuring the timer is cleaned up after completion or
failure. Propagate timeout failures through the existing fallback handling so
publishSkillInventory and getAccountSkillInventories can degrade to local
inventory instead of remaining pending.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@dashboard/src/lib/skills-inventory.ts`:
- Around line 118-120: Update the existing-skill merge logic around
addDeviceSource so it preserves existing.targets exactly and does not union in
remote.targets; only append the device source while retaining remote-only target
state separately where applicable.

In `@dashboard/src/pages/SkillsPage.jsx`:
- Around line 836-854: The loadInstalled auth guard must invalidate in-flight
cloud inventory requests when authentication or cloud sync changes. Increment
cloudInventoryRequest before the guard, and when signedIn, cloud sync, deviceId,
or getAccessToken is unavailable, clear cloud-derived inventory rows and device
metadata before returning; preserve the existing requestId-based stale-response
check for valid cloud loads.

---

Outside diff comments:
In `@dashboard/src/pages/SkillsPage.jsx`:
- Around line 1132-1140: Use the stable identity skill.id || skill.directory
throughout selection and busy-state handling. In
dashboard/src/pages/SkillsPage.jsx at lines 1132-1140, 188, 557-558, 1107-1109,
1146-1148, and 1168-1170, update handleToggleSelect, checked state,
targetBusyKey calls, and selectedIds filters to use that fallback identity; make
the corresponding busy-state update in dashboard/src/pages/SkillDetailPanel.jsx
at line 329.

---

Nitpick comments:
In `@dashboard/src/lib/skills-api.ts`:
- Around line 131-136: Update fetchCloudSkillsJson to use an AbortController
with a bounded timeout when issuing the cloud fetch, ensuring the timer is
cleaned up after completion or failure. Propagate timeout failures through the
existing fallback handling so publishSkillInventory and
getAccountSkillInventories can degrade to local inventory instead of remaining
pending.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9bf0590e-fe14-40cb-91b2-19056e93ad26

📥 Commits

Reviewing files that changed from the base of the PR and between d3bb2e1 and 6d4c36d.

⛔ Files ignored due to path filters (1)
  • dashboard/src/content/copy.csv is excluded by !**/*.csv
📒 Files selected for processing (18)
  • dashboard/edge-patches/tokentracker-account-skills.ts
  • dashboard/src/content/i18n/de/core.json
  • dashboard/src/content/i18n/ja/core.json
  • dashboard/src/content/i18n/ko/core.json
  • dashboard/src/content/i18n/zh-TW/core.json
  • dashboard/src/content/i18n/zh/core.json
  • dashboard/src/lib/skills-api.ts
  • dashboard/src/lib/skills-inventory.test.ts
  • dashboard/src/lib/skills-inventory.ts
  • dashboard/src/pages/SkillDetailPanel.jsx
  • dashboard/src/pages/SkillsPage.jsx
  • dashboard/src/pages/SkillsPage.test.jsx
  • dashboard/vite.config.js
  • migrations/20260721113000_add-device-skill-inventories.sql
  • src/lib/local-api.js
  • src/lib/skills-manager.js
  • test/skills-cloud-guardrails.test.js
  • test/skills-manager-extras.test.js

Comment on lines +118 to +120
const existing = merged[existingIndex];
const targets = [...new Set([...(existing.targets || []), ...(remote.targets || [])])];
merged[existingIndex] = addDeviceSource({ ...existing, targets }, source);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Do not mutate the local targets array with remote targets.

Unioning remote.targets into an existing local skill's targets array corrupts the local state. The targets array of an existing local skill acts as the source of truth for the local UI (e.g., the Detail Panel checkboxes) and API mutations (handleToggleTarget, handleBulkSync). If a target is synced on a remote device but not locally, the union falsely marks it as active in the local skill.targets array. Consequently, the Detail Panel will display it as checked, and any subsequent target toggle will unexpectedly sync the remote target to the local device.

Keep existing.targets unchanged for local skills and only append the device source.

🐛 Proposed fix
-        const existing = merged[existingIndex];
-        const targets = [...new Set([...(existing.targets || []), ...(remote.targets || [])])];
-        merged[existingIndex] = addDeviceSource({ ...existing, targets }, source);
+        const existing = merged[existingIndex];
+        merged[existingIndex] = addDeviceSource(existing, source);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const existing = merged[existingIndex];
const targets = [...new Set([...(existing.targets || []), ...(remote.targets || [])])];
merged[existingIndex] = addDeviceSource({ ...existing, targets }, source);
const existing = merged[existingIndex];
merged[existingIndex] = addDeviceSource(existing, source);
🤖 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 `@dashboard/src/lib/skills-inventory.ts` around lines 118 - 120, Update the
existing-skill merge logic around addDeviceSource so it preserves
existing.targets exactly and does not union in remote.targets; only append the
device source while retaining remote-only target state separately where
applicable.

Comment on lines 836 to +854
const loadInstalled = useCallback(async () => {
const data = await getInstalledSkills();
setInstalledData({ skills: data.skills || [], targets: data.targets || [] });
}, []);
const localData = { skills: data.skills || [], targets: data.targets || [] };
setInstalledData(localData);

const deviceId = getCurrentDeviceId();
if (!signedIn || !getCloudSyncEnabled() || !deviceId || typeof getAccessToken !== "function") return;
const requestId = ++cloudInventoryRequest.current;
// Cloud inventory is a best-effort enrichment. Local Skills render
// immediately and remain usable if auth/network/backend sync is unavailable.
void (async () => {
try {
const accessToken = await getAccessToken();
if (!accessToken) return;
const [cloud] = await Promise.all([
getAccountSkillInventories(accessToken),
publishSkillInventory({ accessToken, deviceId, skills: localData.skills }),
]);
if (cloudInventoryRequest.current !== requestId) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant section and nearby state/effects.
FILE="dashboard/src/pages/SkillsPage.jsx"
wc -l "$FILE"
sed -n '760,920p' "$FILE"

printf '\n--- search: cloudInventoryRequest / setInstalledData / sign-out / cloud sync ---\n'
rg -n "cloudInventoryRequest|setInstalledData|getCloudSyncEnabled|signedIn|getAccessToken|publishSkillInventory|getAccountSkillInventories|setCloud|signOut|sign-in|sign out|cloud sync" "$FILE"

Repository: mm7894215/TokenTracker

Length of output: 7198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read only the specific component region and related state-management helpers.
FILE="dashboard/src/pages/SkillsPage.jsx"

echo "=== file size ==="
wc -l "$FILE"

echo
echo "=== loadInstalled region ==="
sed -n '820,900p' "$FILE"

echo
echo "=== adjacent effect / cleanup regions ==="
sed -n '1,220p' "$FILE" | rg -n "useEffect|useMemo|useCallback|cloudInventoryRequest|setInstalledData|setCloud|signedIn|getCloudSyncEnabled|signOut|auth"

echo
echo "=== all occurrences of cloudInventoryRequest ==="
rg -n "cloudInventoryRequest" "$FILE"

Repository: mm7894215/TokenTracker

Length of output: 3625


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="dashboard/src/pages/SkillsPage.jsx"

printf '=== loadInstalled callers / effects ===\n'
rg -n "loadInstalled\(|loadInitial\(|signedIn|getCloudSyncEnabled|cloudInventoryRequest|setInstalledData\(" "$FILE"

printf '\n=== effect region around initial loading and auth changes ===\n'
sed -n '900,1100p' "$FILE"

printf '\n=== any state clear paths for installedData ===\n'
rg -n "setInstalledData\(|mergeSkillInventories|clear|reset|sign out|signed out|signedIn" "$FILE"

Repository: mm7894215/TokenTracker

Length of output: 8998


Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor

Reachability: External
● Entry
  dashboard/src/pages/SkillsPage.test.jsx
│
▼
● Sink
  dashboard/src/pages/SkillsPage.jsx

Invalidate pending cloud inventory on auth changes cloudInventoryRequest is only bumped after the auth/cloud-sync guard, so a request started before sign-out can still merge the previous account’s cloud inventory into installedData. Bump the generation before the guard and clear cloud-derived rows/device metadata when auth or cloud sync turns off so stale enrichment can’t repopulate a signed-out session.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 838-838: Avoid using the initial state variable in setState
Context: setInstalledData(localData)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 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 `@dashboard/src/pages/SkillsPage.jsx` around lines 836 - 854, The loadInstalled
auth guard must invalidate in-flight cloud inventory requests when
authentication or cloud sync changes. Increment cloudInventoryRequest before the
guard, and when signedIn, cloud sync, deviceId, or getAccessToken is
unavailable, clear cloud-derived inventory rows and device metadata before
returning; preserve the existing requestId-based stale-response check for valid
cloud loads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant