Skip to content

feat(skillhub): add install queue with rate-limit handling and file watcher#445

Merged
alchemistklk merged 25 commits intomainfrom
feat/skillhub-install-queue
Mar 24, 2026
Merged

feat(skillhub): add install queue with rate-limit handling and file watcher#445
alchemistklk merged 25 commits intomainfrom
feat/skillhub-install-queue

Conversation

@alchemistklk
Copy link
Copy Markdown
Contributor

@alchemistklk alchemistklk commented Mar 24, 2026

Summary

  • Install Queue: FIFO queue (max 2 concurrent) replaces the old CONCURRENCY=5 batch loop that exhausted ClawHub's rate limit (120 req/min). Parses rate-limit error messages, pauses the entire queue for the reset duration, and retries up to 5 times per skill.
  • File Watcher: Monitors the skills directory for SKILL.md additions/removals by external actors (OpenClaw agent commands). Detects changes via fs.watch with debounce, syncs the ledger on every startup and on file changes.
  • Queue State API: Catalog response extended with queue array. Frontend shows queued/downloading state on skill cards with toast notifications.
  • Cancel Support: Uninstalling a skill that's currently being installed cancels it cleanly — the onCancelled callback removes any files written by the late-finishing executor.
  • First-Launch Initialization: Static bundled skills are copied and curated skills enqueued only when the skill ledger doesn't exist (fresh install or reinstall). Subsequent restarts skip initialization.
  • Legacy Cleanup: Removed "curated" source type (all skills are now "managed" or "custom"). Legacy ledgers with "curated" are auto-migrated via Zod transform. Removed unused openclawCuratedSkillsDir path.
  • Bug Fixes: Fixed runtime plugin writer .bin symlink crash (EINVAL on fs.cp). Added catalog blocklist for skills no longer available on ClawHub.

Test plan

  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (15 files, 95 tests — 29 new)
  • Manual test: fresh install (27 skills installed, zero duplicates)
  • Manual test: restart does not re-enqueue installed skills
  • Manual test: user install/uninstall via API
  • Manual test: uninstall cancels active install (onCancelled cleanup)
  • Manual test: file watcher detects agent-written/removed skills
  • Manual test: parallel installs produce no duplicate records
  • Manual test: legacy "curated" source migrated to "managed"
  • Manual test: delete ledger + keep skills → syncNow before initialize prevents re-download
  • Manual test: retry failed install works immediately
  • Manual test: invalid slug rejected at route level
  • Manual test: "skill not found" toast shown for unavailable ClawHub skills
  • Two rounds of Codex review — all findings addressed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Background install queue with per-skill statuses (queued, downloading, installing-deps, done, failed), position tracking, and cancellation.
    • UI: queue-aware states, "Skill queued for installation" toast, and dynamic polling while installs are active.
  • Changes

    • Catalog now exposes a queue array alongside installed skills.
    • Install action returns immediate queue info (queued, slug, status, position).
    • Public skill source values: "managed" and "custom" (legacy "curated" removed).

FIFO queue for ClawHub CLI skill installations with max 2 concurrent
downloads, rate-limit detection/retry with queue-wide pause, dedup,
and auto-cleanup of completed items.
…ing uninstalled

When skills are missing from disk, curated records must be fully removed
(not marked uninstalled) so getCuratedSlugsToEnqueue can re-enqueue them.
Marking as uninstalled would cause isRemovedByUser to return true,
permanently blocking re-installation.
fs.cp() with dereference follows node_modules/.bin symlinks which causes
EINVAL when the resolved target overlaps the copy destination. Filter out
.bin directories to prevent the crash.
Three fixes:
- Executor now receives (slug, source) and records in DB after successful install
- getCuratedSlugsToEnqueue checks ALL sources, not just 'curated'
- Prevents re-enqueue on restart when skills were recorded as 'managed' by syncNow
…llsDir fallback

- Check ledger existence before SkillDb.create() to detect first launch
- Only run initialization (copy static skills + enqueue curated) when
  ledger doesn't exist (fresh install or reinstall)
- On subsequent starts, skip initialization entirely — just start watcher
- Add dev fallback for staticSkillsDir pointing to apps/desktop/static/bundled-skills
- All skills (static + curated) go into the same skills/ directory
- Increase fs.watch test timeout to 5s for CI stability
SkillDirWatcher now checks isSlugInFlight() before recording new skills
from disk. Slugs currently in the install queue (queued/downloading) are
skipped — the queue records them with the correct source when complete.
This prevents duplicate records (managed + curated) for the same slug.
- SkillSource type reduced to 'managed' | 'custom'
- All static and ClawHub-installed skills recorded as 'managed'
- Zod schema accepts legacy 'curated' records and transforms to 'managed'
- isRemovedByUser deprecated (always returns false)
- Watcher treats all missing skills the same (markUninstalledBySlugs)
The old bundled-skills destination directory is no longer used. All skills
now go to the unified openclawSkillsDir. The static bundled-skills source
folder (apps/desktop/static/bundled-skills/) is kept for first-launch copy.
… slug at route

Three fixes from code review:
1. syncNow() runs on every startup (not just first launch) to catch
   skills added/removed while controller was stopped
2. InstallQueue.cancel() prevents uninstall from being overridden by
   a completing queue executor
3. Install route validates slug format before enqueuing
…etry

Three fixes from second Codex review:
1. DB record moved from executor to onComplete callback, called only
   after cancel check passes. Cancelled installs never record in DB.
2. syncNow() runs BEFORE initialize() so on-disk skills are recorded
   before curated enqueue checks the ledger.
3. Failed items excluded from dedup so users can retry immediately.
Add errorCode field to QueueItem to classify failures:
- skill_not_found: skill doesn't exist on ClawHub (removed or renamed)
- rate_limit: ClawHub rate limit exceeded
- unknown: other errors

Frontend shows a toast when a queued skill fails with skill_not_found:
'X is not available on ClawHub. It may have been removed or renamed.'
Add CATALOG_BLOCKLIST to hide skills that exist in the ClawHub catalog
data but are no longer available for install (removed or renamed).
Currently blocks: self-improving-agent.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Mar 24, 2026

Deploying nexu-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 73fc5f0
Status: ✅  Deploy successful!
Preview URL: https://ed86a54d.nexu-docs.pages.dev
Branch Preview URL: https://feat-skillhub-install-queue.nexu-docs.pages.dev

View logs

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an async InstallQueue and SkillDirWatcher, removes "curated" as a SkillSource (normalizing legacy data to "managed"), changes installs to enqueue and report queue state via the controller API, updates frontend polling/UI for queue state, and adjusts environment/path handling and tests accordingly.

Changes

Cohort / File(s) Summary
API Schema & Routes
apps/controller/openapi.json, apps/controller/src/routes/skillhub-routes.ts, apps/web/lib/api/types.gen.ts
Removed "curated" from installed skill source; added top-level queue to catalog response; POST /api/v1/skillhub/install now validates slugs, enqueues installs, and returns queue-state fields (queued, slug, status, position).
Install Queue Implementation & Tests
apps/controller/src/services/skillhub/install-queue.ts, tests/desktop/install-queue.test.ts
New InstallQueue with dedupe, bounded concurrency, retries, rate-limit parsing/pauses, cancellation, cleanup timers, and exported helper types; comprehensive unit tests added.
Directory Watcher & Tests
apps/controller/src/services/skillhub/skill-dir-watcher.ts, tests/desktop/skill-dir-watcher.test.ts
New SkillDirWatcher that debounced-watches skills dir for SKILL.md, exposes syncNow(), records on-disk skills as managed, and marks missing ledger slugs uninstalled; lifecycle methods and tests added.
SkillhubService Refactor & Tests
apps/controller/src/services/skillhub-service.ts, apps/controller/tests/skillhub-service.test.ts, tests/controller/skillhub-service.test.ts
Service now composes InstallQueue, SkillDirWatcher, and DB; computes first-launch, copies static skills, enqueues curated slugs as managed, exposes queue and enqueueInstall(), and wires queue callbacks; tests updated for new wiring.
Catalog Manager
apps/controller/src/services/skillhub/catalog-manager.ts
Added executeInstall() (installs without DB recording) and getCuratedSlugsToEnqueue(); removed curatedSkillsDir option; deprecated installCuratedSkills() and normalize curated→managed handling; simplified reconciliation logic.
Types & Re-exports
apps/controller/src/services/skillhub/types.ts, apps/controller/src/services/skillhub/index.ts
Removed "curated" from SkillSource; added QueueItemStatus, QueueErrorCode, QueueItem types; re-exported InstallQueue, SkillDirWatcher, and related types.
Skill DB Changes & Tests
apps/controller/src/services/skillhub/skill-db.ts, tests/desktop/skill-db.test.ts
Zod schema accepts legacy "curated" and normalizes to "managed"; added getAllKnownSlugs(); deprecated getUninstalledCurated() and isRemovedByUser() (now no-op); tests updated to use managed.
Env & Desktop Paths
apps/controller/src/app/env.ts, apps/desktop/main/runtime/manifests.ts, apps/desktop/shared/desktop-paths.ts, related tests
Removed OPENCLAW_CURATED_SKILLS_DIR and env.openclawCuratedSkillsDir; removed getOpenclawCuratedSkillsDir; staticSkillsDir fallback adjusted to bundled-skills path when workspaceRoot available; tests updated to stop providing curated-skills env.
Frontend Types, Hooks & UI
apps/web/src/types/desktop.d.ts, apps/web/src/hooks/use-community-catalog.ts, apps/web/src/components/skills/community-skill-card.tsx, apps/web/src/pages/skills.tsx
Added queue types and SkillhubCatalogData.queue; hooks poll more frequently while active queue items exist and show toast on queued installs; UI components accept queueStatus, reflect queue activity in busy/checked state, hide uninstall while queued, and surface skill_not_found errors.
I18n
apps/web/src/i18n/locales/en.ts, apps/web/src/i18n/locales/zh-CN.ts
Added skills.installQueued and skills.skillNotFound translations.
Docs
docs/plans/2026-03-23-skillhub-install-queue.md
Design doc describing InstallQueue, SkillDirWatcher, catalog/copy/queue startup behavior, and API/frontend changes.
Tests & Test Harness Updates
various test files listed in summary
Added/updated tests for queue, watcher, service behavior; removed curated-skills env from test env builders; numerous test updates for new flows.

Sequence Diagram

sequenceDiagram
    participant Client as Client/Web UI
    participant Routes as skillhub-routes
    participant Queue as InstallQueue
    participant Executor as CatalogManager.executeInstall
    participant Watcher as SkillDirWatcher
    participant DB as SkillDb

    Client->>Routes: POST /api/v1/skillhub/install?slug=foo
    Routes->>Routes: Validate slug (regex)
    Routes->>Queue: enqueue("foo","managed")
    Queue-->>Routes: QueueItem (queued, position)
    Routes->>Client: { queued: true, slug, status: "queued", position }

    Note over Queue,Executor: async drain/execute loop
    Queue->>Executor: executeInstall("foo")
    Executor-->>Queue: success / error

    alt success
        Queue->>DB: onComplete -> recordInstall("foo","managed")
        Queue->>Queue: mark done, schedule cleanup
    else rate_limit
        Queue->>Queue: classify rate_limit, increment retries, pause (3–60s), requeue
    else skill_not_found
        Queue->>Queue: mark failed, set errorCode "skill_not_found"
    end

    par filesystem sync
        Watcher->>Watcher: fs.watch event (SKILL.md)
        Watcher->>Watcher: debounced syncNow()
        Watcher->>DB: recordBulkInstall(newOnDisk, "managed")
        Watcher->>DB: markUninstalledBySlugs(missingFromDisk, source)
    end

    Client->>Routes: GET /api/v1/skillhub/catalog
    Routes->>DB: fetch catalog
    Routes->>Queue: collect queue items
    Routes->>Client: { catalog..., queue: [...] }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • lefarcen
  • nettee

"
🐇 I lined them up, one slug at a time,
Managed not curated — a clearer rhyme.
The watcher listens, the queue hums along,
Retries, pauses, and hops to the song —
Carrots for installs, and a cheerful ding! 🥕
"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature additions: an install queue, rate-limit handling, and a file watcher—all central to this changeset.
Description check ✅ Passed The description provides a comprehensive summary covering what, why, and how; lists affected areas; includes a complete test plan; and addresses the required template sections with substantive content.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/skillhub-install-queue

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
apps/controller/src/services/skillhub/curated-skills.ts (1)

74-77: ⚠️ Potential issue | 🟠 Major

isRemovedByUser always returns false — re-installation bug affects both deprecated and new flows.

Per apps/controller/src/services/skillhub/skill-db.ts:141-142, isRemovedByUser is hardcoded to return false. The guards at lines 74 and 121 in curated-skills.ts are ineffective.

The new replacement mechanism in getCuratedSlugsToEnqueue() (catalog-manager.ts:316-318) has the same issue: it uses getAllInstalled(), which filters only for status === "installed" records. When a user uninstalls a skill, it transitions to status "uninstalled" (skill-db.ts:125) and is no longer returned by getAllInstalled(). Therefore, uninstalled curated skills will be re-enqueued instead of being skipped.

To preserve user uninstall preferences, update getCuratedSlugsToEnqueue() to also exclude slugs with status "uninstalled" from the ledger.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/skillhub/curated-skills.ts` around lines 74 -
77, The guard using skillDb.isRemovedByUser(slug) is ineffective because
isRemovedByUser currently always returns false; instead, update
getCuratedSlugsToEnqueue (and the curated-skills guards) to consult the ledger
and explicitly exclude slugs whose ledger entry status is "uninstalled" (in
addition to "installed" handling). Concretely, modify getCuratedSlugsToEnqueue
to fetch ledger entries (use getAll or the ledger accessor used in skill-db),
filter out any entries where entry.status === "uninstalled" before building the
enqueue list, and/or implement isRemovedByUser to return true when the ledger
has an "uninstalled" record for the slug so existing checks in curated-skills.ts
and catalog-manager.ts work as intended.
🧹 Nitpick comments (5)
docs/plans/2026-03-23-skillhub-install-queue.md (2)

167-169: Plan references "curated" source which is being removed.

The test snippets in this plan document use "curated" as a source value (e.g., lines 167, 291-293), but the PR removes "curated" as a valid SkillSource. If this plan is still being used as a reference, consider updating the examples to use "managed" for consistency with the final implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-23-skillhub-install-queue.md` around lines 167 - 169,
Update the plan examples to stop using the removed "curated" SkillSource: change
calls like queue.enqueue("a", "curated") to use the current valid source (e.g.,
"managed") so examples match the implementation; search for occurrences of
queue.enqueue(...) and any other examples referencing "curated" in this document
and replace them with the canonical SkillSource value used in the codebase.

1306-1311: Add language specifier to fenced code block.

Static analysis flags this block as missing a language identifier (MD040). Since it shows state transitions, consider using text or plaintext.

Suggested fix
-```
+```text
 queued → downloading → done          (happy path)
 queued → downloading → failed        (non-rate-limit error)
 queued → downloading → queued        (rate-limited, retry)
 queued → downloading → queued → ... → failed  (max retries exceeded)
-```
+```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-23-skillhub-install-queue.md` around lines 1306 - 1311,
Add a language identifier to the fenced code block that shows state transitions
so the markdown linter (MD040) is satisfied; change the opening fence from ```
to ```text (or ```plaintext) for the block containing "queued → downloading →
done ..." and ensure the closing fence remains ``` so the block is properly
fenced.
tests/desktop/skill-dir-watcher.test.ts (1)

60-190: Please cover the new watcher race guards directly.

The suite exercises happy-path syncs, but it still doesn’t assert either isSlugInFlight suppression or that stop() cancels a pending debounce. Those are the branches that prevent duplicate ledger writes after queue/install churn, so they deserve direct coverage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/desktop/skill-dir-watcher.test.ts` around lines 60 - 190, Add tests
that assert the watcher’s race guards: create a SkillDirWatcher with a small
debounceMs and a fake/real skillDb, then trigger a sync and while that sync is
running verify that the watcher reports the slug as in-flight (isSlugInFlight
for the skill) and that a concurrent sync attempt does not record a second
install; also add a test that starts the watcher, writes a SKILL.md to schedule
a debounced sync, then immediately calls stop() and assert the pending sync
never runs (the skill is not recorded). Reference SkillDirWatcher, start(),
stop(), syncNow(), debounceMs and isSlugInFlight when locating where to add the
assertions.
apps/web/src/hooks/use-community-catalog.ts (1)

30-35: Use generated SDK types directly; stop casting away their contract.

The casts on lines 35 and 59–62 hide type drift between the generated API (apps/web/lib/api/types.gen.ts) and local DTOs (apps/web/src/types/desktop.d.ts). The generated queue items include a retries field that your local QueueItem type is missing; the source enum also differs ("curated" | "managed" | "custom" locally vs. 'managed' | 'custom' in the SDK). Any code that accesses retries will compile but fail at runtime. Remove the as unknown as and as { ... } casts; use or properly map the generated response types instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/use-community-catalog.ts` around lines 30 - 35, The code
is casting the SDK response away from its generated types (removing type safety)
in the queryFn of use-community-catalog (the useQuery block using
getApiV1SkillhubCatalog and SkillhubCatalogData); remove the "as unknown as
SkillhubCatalogData" and any manual "as { ... }" casts and instead use the
generated SDK types from apps/web/lib/api/types.gen.ts for the API response, or
explicitly map the SDK shape to your local DTO (e.g., convert SDK queue items
that include retries and the SDK source enum into your local QueueItem shape)
inside the queryFn before returning; update the return type of the queryFn (and
any downstream consumers) to the proper SDK type or the mapped local type so you
no longer hide schema drift between getApiV1SkillhubCatalog and your local
types.
apps/controller/tests/skillhub-service.test.ts (1)

210-227: The restart branch is still untested.

Every start() test builds a fresh temp root, so existsSync(env.skillDbPath) never becomes true and the suite only exercises first-launch behavior. Please add one case with a preexisting ledger file so the “skip initialization on subsequent restarts” path is locked down.

Also applies to: 271-434

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/tests/skillhub-service.test.ts` around lines 210 - 227, Add a
test in apps/controller/tests/skillhub-service.test.ts that creates a
preexisting ledger file at env.skillDbPath inside the temp root (use the same
mkdtemp/rootDir setup) before calling start() on the SkillHub service so the
existsSync(env.skillDbPath) branch is exercised; ensure mocks like
mockSkillDbCreate and mockCopyStaticSkills reflect a restart (e.g.,
mockSkillDbCreate not called or skipped) and assert the initialization path was
skipped (check relevant calls/flags on SkillHubService.start, ledger
initialization code, and any "skip initialization" behavior). Also add the same
preexisting-ledger scenario to the other start() tests referenced in the 271-434
range so subsequent restarts are covered consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/openapi.json`:
- Around line 5436-5448: Update the JSON schema for the response properties
"error" and "errorCode" to OpenAPI 3.1 null-preserving syntax: replace the
"nullable": true usage with a type array that includes "null" (i.e., set "type"
to ["string","null"] for "error" and set "type" to ["string","null"] plus
preserve the "enum" for "errorCode"); then regenerate the client types so
apps/web/lib/api/types.gen.ts reflects the nullable types.

In `@apps/controller/src/routes/skillhub-routes.ts`:
- Around line 195-197: The uninstall currently races with active executors
because container.skillhubService.queue.cancel(slug) only marks cancellation;
you must not call container.skillhubService.catalog.uninstallSkill(slug) until
running jobs have stopped. Change the flow to either (a) after
queue.cancel(slug) await an existing wait method (e.g., queue.waitForStop(slug)
/ queue.awaitIdle(slug) / queue.abortAndWait(slug)) to perform an abortable
shutdown, or (b) if no wait API exists, check a running flag (e.g.,
queue.isActive(slug) / queue.hasRunningJobs(slug)) and reject the uninstall with
a 409/appropriate error until the slug is inactive; ensure you reference and use
container.skillhubService.queue.cancel, the chosen wait/check method, and
container.skillhubService.catalog.uninstallSkill so uninstall only runs after
active jobs have fully stopped.
- Around line 152-154: Move the slug pattern from the ad-hoc SLUG_REGEX check
into the shared Zod schema so all routes use the same validation: update
skillhubSlugSchema (the shared schema in packages/shared/src/schemas/) to
include .regex(SLUG_REGEX or the equivalent pattern) instead of only min(1),
remove the inline SLUG_REGEX.test(slug) guard in the route handler
(apps/controller/src/routes/skillhub-routes.ts) and rely on the validated
path/body param schema for POST /install, DELETE /uninstall and GET
/skills/{slug} so OpenAPI/SDK and all routes enforce the same slug rules.

In `@apps/controller/src/services/skillhub-service.ts`:
- Around line 54-59: InstallQueue's callbacks must not violate its error
contract: make onComplete non-throwing by catching any errors from
skillDb.recordInstall(slug, source) (log the failure but do not rethrow), and
change onCancelled to await catalogManager.uninstallSkill(slug) and inspect its
return value; if the result is { ok: false } then throw an error (or return a
rejected promise) so the queue sees the failure. Reference: onComplete,
skillDb.recordInstall, onCancelled, catalogManager.uninstallSkill, and
InstallQueue.

In `@apps/controller/src/services/skillhub/catalog-manager.ts`:
- Around line 282-309: The code canonicalizes the slug inside executeInstall
(slug = SLUG_CORRECTIONS[rawSlug] ?? rawSlug) but the InstallQueue bookkeeping
still uses the original raw slug, causing ledger drift; fix by normalizing
before queue operations or returning/plumbing the canonical slug into the
completion path: either canonicalize rawSlug earlier (where items are enqueued)
so enqueue uses the normalized value, or modify executeInstall/onComplete to
pass the resolved slug back to InstallQueue.complete (or whichever method
records completion) so the ledger records the canonical slug (reference:
executeInstall, SLUG_CORRECTIONS, InstallQueue, onComplete).
- Around line 316-319: The current getCuratedSlugsToEnqueue() uses only
this.db.getAllInstalled() so migrated-but-uninstalled ledger entries are treated
as never-seen; change the logic to exclude any CURATED_SKILL_SLUGS that have any
ledger record instead of only installed rows. Update getCuratedSlugsToEnqueue()
to call the ledger-history API on the DB (e.g. this.db.getLedgerHistory() /
this.db.getLedgerEntriesForSlug(slug) or a new this.db.hasLedgerRecord(slug))
and filter out slugs with any ledger record before returning the list from
CURATED_SKILL_SLUGS. Ensure you reference getCuratedSlugsToEnqueue,
this.db.getAllInstalled, and CURATED_SKILL_SLUGS when making the change.

In `@apps/controller/src/services/skillhub/skill-dir-watcher.ts`:
- Around line 38-43: The syncNow() implementation treats errors from
scanDiskSlugs() as an empty scan which can incorrectly mark installed skills as
uninstalled or stop reconciliation; wrap the call to this.scanDiskSlugs() in a
try/catch inside syncNow(), log or capture the error, and return early on
failure (do not proceed to diff/marking logic). Ensure the catch targets
failures when reading this.skillsDir so syncNow() aborts safely if the directory
disappears or becomes unreadable.

In `@apps/web/src/pages/skills.tsx`:
- Around line 60-64: The current isBusy calculation treats queue activity as a
local pending action and disables the cancel/uninstall UI; update the logic so
queued/downloading/installing-deps do not block the cancel path: either remove
isQueueActive from isBusy (only use pendingAction !== null) or keep
isQueueActive but explicitly enable the uninstall/cancel controls when
queueStatus is queued/downloading/installing-deps so handleToggle(false) /
queue.cancel(slug) remains reachable; apply the same change in the companion
component CommunitySkillCard (look for isQueueActive, isBusy, handleToggle and
queue.cancel references) so queued installs can always be cancelled.

In `@tests/desktop/skill-db.test.ts`:
- Around line 164-165: The guard using the deprecated isRemovedByUser
(referenced in curated-skills.ts around the logic in copyStaticSkills and
resolveCuratedSkillsToInstall) is ineffective and should be removed or replaced:
either delete the conditional checks that rely on isRemovedByUser so the
existing disk-existence fallbacks handle copies/enqueues, or implement a proper
persisted removal-tracking mechanism (e.g., a removal registry or config) and
consult that registry in copyStaticSkills and resolveCuratedSkillsToInstall
instead of isRemovedByUser to honor user-deletions across restarts.

---

Outside diff comments:
In `@apps/controller/src/services/skillhub/curated-skills.ts`:
- Around line 74-77: The guard using skillDb.isRemovedByUser(slug) is
ineffective because isRemovedByUser currently always returns false; instead,
update getCuratedSlugsToEnqueue (and the curated-skills guards) to consult the
ledger and explicitly exclude slugs whose ledger entry status is "uninstalled"
(in addition to "installed" handling). Concretely, modify
getCuratedSlugsToEnqueue to fetch ledger entries (use getAll or the ledger
accessor used in skill-db), filter out any entries where entry.status ===
"uninstalled" before building the enqueue list, and/or implement isRemovedByUser
to return true when the ledger has an "uninstalled" record for the slug so
existing checks in curated-skills.ts and catalog-manager.ts work as intended.

---

Nitpick comments:
In `@apps/controller/tests/skillhub-service.test.ts`:
- Around line 210-227: Add a test in
apps/controller/tests/skillhub-service.test.ts that creates a preexisting ledger
file at env.skillDbPath inside the temp root (use the same mkdtemp/rootDir
setup) before calling start() on the SkillHub service so the
existsSync(env.skillDbPath) branch is exercised; ensure mocks like
mockSkillDbCreate and mockCopyStaticSkills reflect a restart (e.g.,
mockSkillDbCreate not called or skipped) and assert the initialization path was
skipped (check relevant calls/flags on SkillHubService.start, ledger
initialization code, and any "skip initialization" behavior). Also add the same
preexisting-ledger scenario to the other start() tests referenced in the 271-434
range so subsequent restarts are covered consistently.

In `@apps/web/src/hooks/use-community-catalog.ts`:
- Around line 30-35: The code is casting the SDK response away from its
generated types (removing type safety) in the queryFn of use-community-catalog
(the useQuery block using getApiV1SkillhubCatalog and SkillhubCatalogData);
remove the "as unknown as SkillhubCatalogData" and any manual "as { ... }" casts
and instead use the generated SDK types from apps/web/lib/api/types.gen.ts for
the API response, or explicitly map the SDK shape to your local DTO (e.g.,
convert SDK queue items that include retries and the SDK source enum into your
local QueueItem shape) inside the queryFn before returning; update the return
type of the queryFn (and any downstream consumers) to the proper SDK type or the
mapped local type so you no longer hide schema drift between
getApiV1SkillhubCatalog and your local types.

In `@docs/plans/2026-03-23-skillhub-install-queue.md`:
- Around line 167-169: Update the plan examples to stop using the removed
"curated" SkillSource: change calls like queue.enqueue("a", "curated") to use
the current valid source (e.g., "managed") so examples match the implementation;
search for occurrences of queue.enqueue(...) and any other examples referencing
"curated" in this document and replace them with the canonical SkillSource value
used in the codebase.
- Around line 1306-1311: Add a language identifier to the fenced code block that
shows state transitions so the markdown linter (MD040) is satisfied; change the
opening fence from ``` to ```text (or ```plaintext) for the block containing
"queued → downloading → done ..." and ensure the closing fence remains ``` so
the block is properly fenced.

In `@tests/desktop/skill-dir-watcher.test.ts`:
- Around line 60-190: Add tests that assert the watcher’s race guards: create a
SkillDirWatcher with a small debounceMs and a fake/real skillDb, then trigger a
sync and while that sync is running verify that the watcher reports the slug as
in-flight (isSlugInFlight for the skill) and that a concurrent sync attempt does
not record a second install; also add a test that starts the watcher, writes a
SKILL.md to schedule a debounced sync, then immediately calls stop() and assert
the pending sync never runs (the skill is not recorded). Reference
SkillDirWatcher, start(), stop(), syncNow(), debounceMs and isSlugInFlight when
locating where to add the assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a7460088-1c62-48ca-81b4-bcac03be24ff

📥 Commits

Reviewing files that changed from the base of the PR and between e2e33d8 and e39c794.

📒 Files selected for processing (31)
  • apps/controller/openapi.json
  • apps/controller/src/app/env.ts
  • apps/controller/src/routes/skillhub-routes.ts
  • apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts
  • apps/controller/src/services/skillhub-service.ts
  • apps/controller/src/services/skillhub/catalog-manager.ts
  • apps/controller/src/services/skillhub/curated-skills.ts
  • apps/controller/src/services/skillhub/index.ts
  • apps/controller/src/services/skillhub/install-queue.ts
  • apps/controller/src/services/skillhub/skill-db.ts
  • apps/controller/src/services/skillhub/skill-dir-watcher.ts
  • apps/controller/src/services/skillhub/types.ts
  • apps/controller/tests/sessions-runtime.test.ts
  • apps/controller/tests/skillhub-service.test.ts
  • apps/desktop/main/runtime/manifests.ts
  • apps/desktop/shared/desktop-paths.ts
  • apps/web/lib/api/types.gen.ts
  • apps/web/src/components/skills/community-skill-card.tsx
  • apps/web/src/hooks/use-community-catalog.ts
  • apps/web/src/i18n/locales/en.ts
  • apps/web/src/i18n/locales/zh-CN.ts
  • apps/web/src/pages/skills.tsx
  • apps/web/src/types/desktop.d.ts
  • docs/plans/2026-03-23-skillhub-install-queue.md
  • tests/controller/skillhub-service.test.ts
  • tests/desktop/install-queue.test.ts
  • tests/desktop/model-provider-service.test.ts
  • tests/desktop/openclaw-auth-profiles-writer.test.ts
  • tests/desktop/openclaw-config-compiler.test.ts
  • tests/desktop/skill-db.test.ts
  • tests/desktop/skill-dir-watcher.test.ts
💤 Files with no reviewable changes (5)
  • apps/desktop/shared/desktop-paths.ts
  • tests/desktop/openclaw-config-compiler.test.ts
  • tests/desktop/model-provider-service.test.ts
  • tests/desktop/openclaw-auth-profiles-writer.test.ts
  • apps/controller/tests/sessions-runtime.test.ts

Comment thread apps/controller/openapi.json
Comment thread apps/controller/src/routes/skillhub-routes.ts Outdated
Comment thread apps/controller/src/routes/skillhub-routes.ts Outdated
Comment thread apps/controller/src/services/skillhub-service.ts
Comment thread apps/controller/src/services/skillhub/catalog-manager.ts
Comment thread apps/controller/src/services/skillhub/catalog-manager.ts
Comment thread apps/controller/src/services/skillhub/skill-dir-watcher.ts
Comment thread apps/web/src/pages/skills.tsx Outdated
Comment thread tests/desktop/skill-db.test.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/controller/openapi.json (1)

6660-6671: ⚠️ Potential issue | 🟠 Major

Model queue error nullability with valid OpenAPI 3.1 unions.

These new fields still use nullable: true, which is the 3.0 form. In a 3.1 schema that can leave generated client types non-nullable, and errorCode still needs an explicit null branch to make returned null values valid.

🛠️ Suggested schema change
                           "error": {
-                            "type": "string",
-                            "nullable": true
+                            "type": ["string", "null"]
                           },
                           "errorCode": {
-                            "type": "string",
-                            "nullable": true,
-                            "enum": [
-                              "skill_not_found",
-                              "rate_limit",
-                              "unknown"
-                            ]
+                            "oneOf": [
+                              {
+                                "type": "string",
+                                "enum": [
+                                  "skill_not_found",
+                                  "rate_limit",
+                                  "unknown"
+                                ]
+                              },
+                              {
+                                "type": "null"
+                              }
+                            ]
                           },
In OpenAPI 3.1 / JSON Schema 2020-12, what is the correct schema shape for (1) a nullable string field and (2) a nullable string enum field so both schema validation and generated TypeScript clients preserve `null`?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/openapi.json` around lines 6660 - 6671, The OpenAPI 3.1
schema is still using the 3.0 `nullable: true` form for the "error" and
"errorCode" properties; replace that with JSON Schema 2020-12 style nullability
so generated clients preserve null: for the simple nullable string property
"error" change its schema to allow both string and null (e.g., type as
["string","null"] or an anyOf/oneOf of string and {"type":"null"}), and for the
nullable enum "errorCode" either include null as one of the enum values (e.g.,
enum: ["skill_not_found","rate_limit","unknown", null]) or use a oneOf/anyOf
combining the enum and {"type":"null"} so null is a valid branch.
apps/web/src/pages/skills.tsx (1)

63-67: ⚠️ Potential issue | 🟠 Major

Keep queue-active installs cancellable from the card.

isQueueActive now feeds isBusy, so the switch is disabled and the uninstall control disappears exactly when the backend supports uninstall-as-cancel. That removes the only UI path to stop a queued/download/install-deps job.

🛠️ Suggested fix
   const isQueueActive =
     queueStatus === "queued" ||
     queueStatus === "downloading" ||
     queueStatus === "installing-deps";
-  const isBusy = pendingAction !== null || isQueueActive;
+  const isBusy = pendingAction !== null;

 ...
             <Switch
               size="xs"
               checked={isInstalled || isQueueActive}
               disabled={isBusy}
-              loading={isBusy}
+              loading={isBusy || isQueueActive}
               onCheckedChange={handleToggle}
             />
-            {isInstalled && !isQueueActive ? (
-              <button
-                type="button"
-                onClick={(e) => {
-                  e.preventDefault();
-                  e.stopPropagation();
-                  handleToggle(false);
-                }}
-                disabled={isBusy}
-                className="text-[12px] font-medium text-text-muted hover:text-[var(--color-danger)] transition-colors"
-              >
-                Uninstall
-              </button>
-            ) : (
-              <span className="text-[11px] text-text-muted">Installing…</span>
-            )}
+            <button
+              type="button"
+              onClick={(e) => {
+                e.preventDefault();
+                e.stopPropagation();
+                handleToggle(false);
+              }}
+              disabled={isBusy}
+              className="text-[12px] font-medium text-text-muted hover:text-[var(--color-danger)] transition-colors"
+            >
+              {isQueueActive ? "Cancel" : "Uninstall"}
+            </button>

Also applies to: 151-175

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/pages/skills.tsx` around lines 63 - 67, The current logic sets
isQueueActive (queueStatus === "queued" || "downloading" || "installing-deps")
and then uses it to compute isBusy, which hides/disables the uninstall/cancel
control when a job is queued/downloads/installing-deps; change the logic so
queued/download/install-deps remain cancellable: keep isQueueActive for UI
status but compute isBusy without treating those cancellable queue states as
busy (e.g., create isCancellableQueue = queueStatus === "queued" || queueStatus
=== "downloading" || queueStatus === "installing-deps" and set isBusy =
pendingAction !== null || (queueStatus not in cancellable states && other
long-running states)), ensuring the switch/uninstall control renders/enabled
when isCancellableQueue is true while preserving busy state for truly blocking
statuses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/pages/skills.tsx`:
- Around line 267-279: The current de-duplication Set shownErrorSlugs in the
useEffect keys only by item.slug so a repeat failure for the same slug in a
later attempt is suppressed; update the logic to deduplicate by attempt (e.g.,
combine item.slug + item.enqueuedAt or item.attemptId) or remove the slug from
shownErrorSlugs when the item leaves the "failed" state. Locate the useEffect
that iterates data?.queue and change the key used for shownErrorSlugs.current
(or add logic to clear entries when item.status !== "failed") so each new
enqueue attempt will surface its toast.

---

Duplicate comments:
In `@apps/controller/openapi.json`:
- Around line 6660-6671: The OpenAPI 3.1 schema is still using the 3.0
`nullable: true` form for the "error" and "errorCode" properties; replace that
with JSON Schema 2020-12 style nullability so generated clients preserve null:
for the simple nullable string property "error" change its schema to allow both
string and null (e.g., type as ["string","null"] or an anyOf/oneOf of string and
{"type":"null"}), and for the nullable enum "errorCode" either include null as
one of the enum values (e.g., enum: ["skill_not_found","rate_limit","unknown",
null]) or use a oneOf/anyOf combining the enum and {"type":"null"} so null is a
valid branch.

In `@apps/web/src/pages/skills.tsx`:
- Around line 63-67: The current logic sets isQueueActive (queueStatus ===
"queued" || "downloading" || "installing-deps") and then uses it to compute
isBusy, which hides/disables the uninstall/cancel control when a job is
queued/downloads/installing-deps; change the logic so
queued/download/install-deps remain cancellable: keep isQueueActive for UI
status but compute isBusy without treating those cancellable queue states as
busy (e.g., create isCancellableQueue = queueStatus === "queued" || queueStatus
=== "downloading" || queueStatus === "installing-deps" and set isBusy =
pendingAction !== null || (queueStatus not in cancellable states && other
long-running states)), ensuring the switch/uninstall control renders/enabled
when isCancellableQueue is true while preserving busy state for truly blocking
statuses.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c56db03-3d4e-4861-b35d-85d231236bb1

📥 Commits

Reviewing files that changed from the base of the PR and between e39c794 and 73fc5f0.

📒 Files selected for processing (8)
  • apps/controller/openapi.json
  • apps/controller/src/app/env.ts
  • apps/controller/tests/sessions-runtime.test.ts
  • apps/desktop/main/runtime/manifests.ts
  • apps/web/lib/api/types.gen.ts
  • apps/web/src/i18n/locales/en.ts
  • apps/web/src/i18n/locales/zh-CN.ts
  • apps/web/src/pages/skills.tsx
💤 Files with no reviewable changes (1)
  • apps/controller/tests/sessions-runtime.test.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/i18n/locales/zh-CN.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/src/i18n/locales/en.ts
  • apps/controller/src/app/env.ts
  • apps/web/lib/api/types.gen.ts

Comment thread apps/web/src/pages/skills.tsx
getCuratedSlugsToEnqueue now uses getAllKnownSlugs() which checks both
installed AND uninstalled records. Previously it only checked installed
records, so user-uninstalled curated skills would be re-enqueued on
next first-launch startup.
…canonicalization

Three fixes from CodeRabbit review:
1. Move slug regex to skillhubSlugSchema so all routes validate consistently
2. Wrap onComplete callback in try/catch to prevent queue crash on DB failure
3. Canonicalize slug before enqueuing to prevent ledger/disk mismatch
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/controller/src/routes/skillhub-routes.ts (1)

191-193: ⚠️ Potential issue | 🔴 Critical

Don't uninstall while an active install is still writing.

queue.cancel(slug) only marks active work as cancelled; it does not wait for executeInstall() / npm install to stop. Calling catalog.uninstallSkill(slug) immediately can delete the directory mid-write and let late files reappear. Either wait until the queue item is inactive, or throw HTTPException(409, ...) here and let onCancelled finish cleanup.

As per coding guidelines: Errors must throw HTTPException with status + contextual message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/routes/skillhub-routes.ts` around lines 191 - 193, The
current code calls container.skillhubService.queue.cancel(slug) then immediately
container.skillhubService.catalog.uninstallSkill(slug), which can delete files
while executeInstall() / npm install is still running; change this to detect an
active install and either (A) wait for the queue item to become inactive/settled
(use the queue API to poll or await the item’s completion) before calling
catalog.uninstallSkill(slug), or (B) immediately throw new HTTPException(409,
"Skill is currently installing; cancel requested and uninstall deferred") so
onCancelled can finish cleanup; ensure you reference and use
container.skillhubService.queue.cancel, the queue item status check,
executeInstall/onCancelled semantics, and throw HTTPException with status and
contextual message if choosing the 409 path.
🧹 Nitpick comments (1)
apps/controller/tests/skillhub-service.test.ts (1)

366-381: Add a canonicalization regression case.

These tests only use identity slugs, so they will still pass if SkillhubService stops calling catalog.canonicalizeSlug() before enqueueing. Making the mock rewrite a slug in both the startup and enqueueInstall() paths would pin the ledger/disk drift fix.

Also applies to: 438-449

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/tests/skillhub-service.test.ts` around lines 366 - 381, The
test should assert that SkillhubService canonicalizes slugs before enqueueing:
update the mock catalog to return ["alpha","beta"] from getCuratedSlugsToEnqueue
and also mock catalog.canonicalizeSlug to rewrite those values (e.g., "alpha" ->
"ALPHA", "beta" -> "BETA"), then call SkillhubService.create(...) and start()
and assert that installQueue.enqueue was called with the canonicalized slugs
(e.g., "ALPHA","BETA") and "managed"; also apply the same canonicalization
rewrite/assertion to the other test block around lines 438-449 that covers
enqueueInstall()/related behavior so the regression is pinned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/src/services/skillhub/catalog-manager.ts`:
- Around line 754-759: refreshCatalog() currently persists meta.skillCount from
the raw skills list before the post-processing that applies CATALOG_BLOCKLIST
and SLUG_CORRECTIONS, causing meta.skillCount to disagree with the filtered
result returned by the reader; update refreshCatalog() to apply the same
filtering/canonicalization (use CATALOG_BLOCKLIST and SLUG_CORRECTIONS) to the
skills array before computing/persisting meta.skillCount, or else compute
meta.skillCount from the final filtered array you intend to cache/return so the
persisted meta.skillCount equals the length of the filtered skills.
- Around line 292-307: The code currently logs raw child-process output from
execFileAsync (stdout/stderr) when invoking clawHubBin which can leak sensitive
URLs or tokens; change the logging in the block that calls execFileAsync so that
instead of this.log("info", `install stdout ${slug}: ${stdout.trim()}`) and
this.log("warn", `install stderr ${slug}: ${stderr.trim()}`) you produce a
sanitized, structured summary: log success/failure for the slug, include a
truncated length or redacted indicator (e.g., "<redacted-output>" or first N
chars only), remove or mask any URLs or tokens, and preserve the existing log
levels via this.log; keep exec errors intact but ensure any attached output is
sanitized before logging.

In `@apps/controller/src/services/skillhub/install-queue.ts`:
- Around line 255-295: When the executor rejects, first check and handle
cancelled installs before any retry/failure logic: if item.cancelled is true,
clear the cancelled flag, call item.onCancelled(), set item.status to
"cancelled" (or appropriate state), remove it from this.active, push it to
this.completed, call this.scheduleCleanup(item) and this.drain(), then return —
do this at the top of the (err: unknown) => { ... } handler so rate-limit
branches (parseRateLimitPauseMs/classifyError/retries/pauseQueue) never run for
cancelled jobs.

---

Duplicate comments:
In `@apps/controller/src/routes/skillhub-routes.ts`:
- Around line 191-193: The current code calls
container.skillhubService.queue.cancel(slug) then immediately
container.skillhubService.catalog.uninstallSkill(slug), which can delete files
while executeInstall() / npm install is still running; change this to detect an
active install and either (A) wait for the queue item to become inactive/settled
(use the queue API to poll or await the item’s completion) before calling
catalog.uninstallSkill(slug), or (B) immediately throw new HTTPException(409,
"Skill is currently installing; cancel requested and uninstall deferred") so
onCancelled can finish cleanup; ensure you reference and use
container.skillhubService.queue.cancel, the queue item status check,
executeInstall/onCancelled semantics, and throw HTTPException with status and
contextual message if choosing the 409 path.

---

Nitpick comments:
In `@apps/controller/tests/skillhub-service.test.ts`:
- Around line 366-381: The test should assert that SkillhubService canonicalizes
slugs before enqueueing: update the mock catalog to return ["alpha","beta"] from
getCuratedSlugsToEnqueue and also mock catalog.canonicalizeSlug to rewrite those
values (e.g., "alpha" -> "ALPHA", "beta" -> "BETA"), then call
SkillhubService.create(...) and start() and assert that installQueue.enqueue was
called with the canonicalized slugs (e.g., "ALPHA","BETA") and "managed"; also
apply the same canonicalization rewrite/assertion to the other test block around
lines 438-449 that covers enqueueInstall()/related behavior so the regression is
pinned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32c99af0-ffaf-4a1c-97d7-831a140cb201

📥 Commits

Reviewing files that changed from the base of the PR and between 18ad3a4 and d4e929d.

📒 Files selected for processing (5)
  • apps/controller/src/routes/skillhub-routes.ts
  • apps/controller/src/services/skillhub-service.ts
  • apps/controller/src/services/skillhub/catalog-manager.ts
  • apps/controller/src/services/skillhub/install-queue.ts
  • apps/controller/tests/skillhub-service.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/controller/src/services/skillhub-service.ts

Comment thread apps/controller/src/services/skillhub/catalog-manager.ts
Comment thread apps/controller/src/services/skillhub/catalog-manager.ts
Comment thread apps/controller/src/services/skillhub/install-queue.ts
…ive skills

Two fixes from CodeRabbit review:
1. scanDiskSlugs returns null on error instead of []. syncNow bails
   out entirely, preventing false 'all skills removed' reconciliation.
2. Switch disabled only during local mutation, not during queue activity.
   Users can toggle OFF to cancel a queued/downloading install.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/web/src/pages/skills.tsx (1)

267-279: ⚠️ Potential issue | 🟡 Minor

Deduplicate not-found toasts per attempt, not per slug.

shownErrorSlugs never clears, so a second failed install of the same slug during the same page session stays silent. Key the set by attempt metadata if the queue exposes it, or remove the slug once it leaves the failed state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/pages/skills.tsx` around lines 267 - 279, The current dedupe
uses shownErrorSlugs (and shownErrorSlugs.current) which never clears, so
repeated failed installs for the same slug in one session won't show a new
toast; update the logic in the useEffect around data?.queue to deduplicate per
install attempt instead of per slug by using an attempt-specific key (e.g.,
item.attemptId or item.createdAt) when available (use shownErrorAttempts instead
of shownErrorSlugs and key by item.attemptId/item.createdAt), or if the queue
has no attempt ID then remove the slug from the set when that item leaves the
failed state (detect item.status !== "failed" and delete item.slug from
shownErrorSlugs.current) before adding on new failures; adjust references to
shownErrorSlugs, useEffect, data?.queue, item.status, item.slug, and toast.error
accordingly.
🧹 Nitpick comments (1)
apps/web/src/pages/skills.tsx (1)

41-53: Share the queue-status source of truth.

These literals are now maintained in multiple places in this file, and they also need to stay aligned with ACTIVE_QUEUE_STATUSES in apps/web/src/hooks/use-community-catalog.ts:17-27. Extracting a shared QueueStatus/active-status helper would keep polling and card state from drifting.

Also applies to: 63-66, 255-259

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/pages/skills.tsx` around lines 41 - 53, The file repeats the
same queue-status string literals (e.g., in the prop type for the Skills card
and elsewhere) that must stay aligned with ACTIVE_QUEUE_STATUSES in
use-community-catalog.ts; extract a single shared QueueStatus type (string union
or enum) and an ACTIVE_QUEUE_STATUSES helper in a common module (or export the
existing one from use-community-catalog.ts), replace the inline literals in the
Skills component (props: queueStatus) and any other occurrences (e.g., card
state checks and polling logic) to import and use the shared QueueStatus type
and ACTIVE_QUEUE_STATUSES constant so all places reference the same source of
truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/src/services/skillhub/skill-dir-watcher.ts`:
- Around line 98-113: The watcher setup using watch(...) with recursive: true
can silently fail on Node.js 20.0–20.12 and the error handler on this.watcher
only logs errors without recovery; update project metadata to require a
compatible Node.js by adding "engines": { "node": ">=20.13" } to
apps/controller/package.json (or add clear documentation about the limitation),
and add watcher error recovery in skill-dir-watcher.ts: when this.watcher emits
"error" call this.watcher.close() (or clear this.watcher), log the failure via
this.log, and schedule a restart of the watcher (e.g., setTimeout to recreate
the watch using the same watch(...) call and reattach the "error" handler) so
scheduleSync and the watch lifecycle are restored after transient failures.

In `@apps/web/src/pages/skills.tsx`:
- Around line 160-175: The new visible strings "Uninstall" and "Installing…" are
not localized; update the JSX in the conditional that uses isInstalled,
isQueueActive, isMutating and handleToggle to call the translation helper (t)
for both labels — e.g. replace the literal "Uninstall" with
t('skills.uninstall') (or the appropriate key used in your app) and replace
"Installing…" with t('skills.installing') — ensure the component imports and
uses the same t function/namespace as other strings in this file so the button
label and fallback span render localized text.

---

Duplicate comments:
In `@apps/web/src/pages/skills.tsx`:
- Around line 267-279: The current dedupe uses shownErrorSlugs (and
shownErrorSlugs.current) which never clears, so repeated failed installs for the
same slug in one session won't show a new toast; update the logic in the
useEffect around data?.queue to deduplicate per install attempt instead of per
slug by using an attempt-specific key (e.g., item.attemptId or item.createdAt)
when available (use shownErrorAttempts instead of shownErrorSlugs and key by
item.attemptId/item.createdAt), or if the queue has no attempt ID then remove
the slug from the set when that item leaves the failed state (detect item.status
!== "failed" and delete item.slug from shownErrorSlugs.current) before adding on
new failures; adjust references to shownErrorSlugs, useEffect, data?.queue,
item.status, item.slug, and toast.error accordingly.

---

Nitpick comments:
In `@apps/web/src/pages/skills.tsx`:
- Around line 41-53: The file repeats the same queue-status string literals
(e.g., in the prop type for the Skills card and elsewhere) that must stay
aligned with ACTIVE_QUEUE_STATUSES in use-community-catalog.ts; extract a single
shared QueueStatus type (string union or enum) and an ACTIVE_QUEUE_STATUSES
helper in a common module (or export the existing one from
use-community-catalog.ts), replace the inline literals in the Skills component
(props: queueStatus) and any other occurrences (e.g., card state checks and
polling logic) to import and use the shared QueueStatus type and
ACTIVE_QUEUE_STATUSES constant so all places reference the same source of truth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 516553fe-c7e2-4417-be62-19f134f06ff2

📥 Commits

Reviewing files that changed from the base of the PR and between d4e929d and dd54040.

📒 Files selected for processing (3)
  • apps/controller/src/services/skillhub/skill-dir-watcher.ts
  • apps/web/src/components/skills/community-skill-card.tsx
  • apps/web/src/pages/skills.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/components/skills/community-skill-card.tsx

Comment thread apps/controller/src/services/skillhub/skill-dir-watcher.ts
Comment thread apps/web/src/pages/skills.tsx
… cleanup failure

- Add cancelInstall() to SkillhubService that canonicalizes slug before
  cancelling (consistent with enqueueInstall)
- onCancelled throws when uninstallSkill returns ok:false instead of
  silently ignoring cleanup failure
- Route uses cancelInstall() instead of queue.cancel() directly
@alchemistklk
Copy link
Copy Markdown
Contributor Author

/cr

@slack-code-review-channel
Copy link
Copy Markdown

✅ CR topic created in Feishu topic group Refly CR.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants