Skip to content

fix(apps): sync installed_apps + installed_meta on MCP/CLI install - #88

Merged
KrasimirKralev merged 4 commits into
ID-Robots:betafrom
KrasimirKralev:fix/apps-install-preferences-sync
Apr 19, 2026
Merged

KrasimirKralev merged 4 commits into
ID-Robots:betafrom
KrasimirKralev:fix/apps-install-preferences-sync

Conversation

@KrasimirKralev

@KrasimirKralev KrasimirKralev commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Installs triggered via the MCP app_install tool (or CLI) landed the skill on disk but were invisible on the desktop and in the Store's Installed tab. The Store UI filters "Installed" by the installed_apps preference, and the desktop filters icons by both installed_apps AND installed_meta; those were only written as a client-side side-effect of the AppStore UI install flow. Off-desktop installs flew under that radar.

This PR makes the server-side routes the source of truth:

  • src/app/setup-api/apps/install/route.ts now writes both pref:installed_apps (id) and pref:installed_meta (name + color + iconUrl)
  • src/app/setup-api/apps/uninstall/route.ts mirrors the delete
  • src/lib/store-categories.ts (new) holds the category→color map previously inlined in AppStore.tsx, so client and server stay in step
  • src/app/page.tsx and src/components/AppStore.tsx import the shared InstalledMeta type / colour map

Behaviour

  • Fresh install (MCP or UI): skill on disk + both prefs populated → appears on desktop and Store after page refresh
  • Re-install: short-circuits — no Store fetch, no config write, so MCP retries don't thrash the upstream API
  • Offline / Store unreachable: falls back to a title-cased slug + neutral color + local icon path; app still renders rather than being silently dropped
  • Uninstall: both prefs cleaned up

Scope / non-goals

Live refresh is not in this PR. The desktop's React state is seeded on mount, so newly installed skills appear after a hard refresh (Ctrl+Shift+R). Wiring server→client push (SSE) is a deferred follow-up — kept this PR small and focused on the correctness fix.

Test plan

  • MCP app_install via Gemma chat: skill on disk + preferences populated (verified on Jetson at 192.168.1.57)
  • After page refresh: skill shows in Store's Installed tab and on the desktop with correct icon + color
  • Re-install of already-installed app: no Store API fetch, no config write (short-circuit)
  • UI install still works (regression check — same meta populated via existing client path)
  • Uninstall via MCP: prefs cleaned up → skill disappears after refresh

Summary by CodeRabbit

  • New Features

    • App installs now fetch and store app metadata (name, color, icon, optional launch URL) with deterministic fallbacks.
  • Bug Fixes

    • Uninstall now removes associated metadata and installed-app entries to avoid orphaned preferences.
  • Improvements

    • Centralized category color definitions for consistent styling and matched store metadata synchronization.

…stall

Installs triggered via the MCP `app_install` tool (or CLI) landed
the skill on disk, reloaded the gateway, and returned success — but
the desktop never saw them. The Store UI's 'Installed' tab and the
desktop icon grid both read from the `installed_apps` / `installed_meta`
preferences, and those were only written by the AppStore React component
as a client-side side-effect of the UI install flow. Off-desktop
installs (MCP, CLI) flew under that radar.

The install route now writes both preferences as the server-side
source of truth. Metadata (name, color, iconUrl) is derived by looking
up the Store listing for the appId; the category → color map that the
UI uses is extracted to `src/lib/store-categories.ts` so both sides
stay in step. Falls back to a title-cased slug and a neutral color
when the Store API is unreachable. Uninstall mirrors the delete.

Re-installs short-circuit — if the app is already listed and has
meta, no Store fetch and no config write, so MCP retries don't
thrash the upstream API.

The desktop still needs a page refresh to see new installs (the React
state is only seeded on mount). Live-refresh via SSE is deferred to
a follow-up PR.

Live-tested on Jetson: Gemma-initiated MCP `app_install` → skill on
disk → preferences updated → skill appears on desktop + Store tab
after refresh. Covers both fresh installs and already-installed
(backfill) cases.
@KrasimirKralev
KrasimirKralev requested a review from a team as a code owner April 19, 2026 12:18
@KrasimirKralev

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@KrasimirKralev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 53 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 53 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 83bb2c1d-a09c-4c00-897e-1b84146eafea

📥 Commits

Reviewing files that changed from the base of the PR and between 7a1f869 and 1b59255.

📒 Files selected for processing (2)
  • src/app/setup-api/apps/install/route.ts
  • src/tests/routes/apps/install.test.ts
📝 Walkthrough

Walkthrough

Centralized store category metadata and an InstalledMeta type; installation now looks up remote store metadata (with fallback) and syncs pref:installed_apps / pref:installed_meta; uninstallation removes app entries from those prefs; UI component switched to use shared category constants.

Changes

Cohort / File(s) Summary
Store metadata & types
src/lib/store-categories.ts
New module exporting CATEGORY_COLORS, DEFAULT_CATEGORY_COLOR, and InstalledMeta interface.
Page state typing
src/app/page.tsx
Switched local installedMeta state to use the InstalledMeta type import and tightened casts when loading preferences.
AppStore UI
src/components/AppStore.tsx
Removed local category color map; now imports CATEGORY_COLORS / DEFAULT_CATEGORY_COLOR and uses them for StoreApp.color.
Install flow & config sync
src/app/setup-api/apps/install/route.ts
Added store-metadata lookup helpers (with 8s timeout and deterministic fallbacks). On successful install, reads pref:installed_apps/pref:installed_meta, conditionally writes updated metadata via configSetMany, and appends appId to pref:installed_apps if missing. Logs warnings on lookup/sync failures.
Uninstall cleanup
src/app/setup-api/apps/uninstall/route.ts
After uninstall, reads prefs and removes the appId from pref:installed_apps and deletes its key from pref:installed_meta, persisting changes with configSetMany; failures emit warnings but do not abort uninstall.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant InstallAPI as Install API
    participant StoreAPI as Store API
    participant ConfigStore as Config Store
    participant Gateway as Gateway

    Client->>InstallAPI: POST /setup-api/apps/install
    InstallAPI->>InstallAPI: Run 'openclaw skills install'
    alt install succeeds
        InstallAPI->>StoreAPI: Fetch app metadata (timeout: 8s)
        alt Store metadata available
            StoreAPI-->>InstallAPI: Return metadata (name, category, icon)
        else lookup fails
            InstallAPI->>InstallAPI: Build deterministic InstalledMeta fallback
        end
        InstallAPI->>ConfigStore: getAll(pref:installed_apps, pref:installed_meta)
        ConfigStore-->>InstallAPI: Return existing prefs
        InstallAPI->>InstallAPI: Merge/append appId and set metadata if missing
        InstallAPI->>ConfigStore: setMany(updated prefs)
        ConfigStore-->>InstallAPI: Persisted
        InstallAPI->>Gateway: reloadGateway()
        InstallAPI-->>Client: Success
    else install fails
        InstallAPI-->>Client: Error
    end
Loading
sequenceDiagram
    participant Client as Client
    participant UninstallAPI as Uninstall API
    participant FS as File System
    participant ConfigStore as Config Store
    participant Gateway as Gateway

    Client->>UninstallAPI: POST /setup-api/apps/uninstall
    UninstallAPI->>FS: Remove skill directory
    UninstallAPI->>FS: Delete app icon
    UninstallAPI->>ConfigStore: getAll(pref:installed_apps, pref:installed_meta)
    ConfigStore-->>UninstallAPI: Return existing prefs
    UninstallAPI->>UninstallAPI: Filter appId out of installed_apps
    UninstallAPI->>UninstallAPI: Delete appId key from installed_meta
    alt prefs changed
        UninstallAPI->>ConfigStore: setMany(updated prefs)
        ConfigStore-->>UninstallAPI: Persisted
    else no changes needed
        UninstallAPI->>UninstallAPI: Skip persistence
    end
    UninstallAPI->>Gateway: reloadGateway()
    UninstallAPI-->>Client: Success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through colors, names, and keys,
Fetching icons on breeze-borne pleas,
Syncing lists both near and far,
Stashing metadata in the jar,
Now installs and removals hum like bees.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 directly describes the main change: syncing installed_apps and installed_meta preferences on MCP/CLI install, which is the core objective of this PR.
Description check ✅ Passed The description comprehensively covers the Summary, Type of change (Bug fix), test plan, and behavior details. However, it lacks explicit checkboxes for testing (bun run lint, test, build) and branch/convention compliance.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/setup-api/apps/install/route.ts`:
- Around line 99-125: Add a short inline comment above the re-install
short-circuit explaining that when lookupStoreMeta returns the title-cased
fallback it becomes sticky in pref:installed_meta (so subsequent re-installs
will not refresh real store metadata), and if you prefer automatic refresh
instead of a comment, change the condition in the if (!alreadyListed ||
!metaMap[appId]) check to also detect the fallback (e.g. add an isFallback check
comparing meta.color === DEFAULT_CATEGORY_COLOR && meta.name ===
titleCaseFromSlug(appId)) and allow a refetch by treating fallback as missing;
reference lookupStoreMeta, pref:installed_meta, pref:installed_apps,
configSetMany, DEFAULT_CATEGORY_COLOR and titleCaseFromSlug when making the
change.
- Around line 10-14: The titleCaseFromSlug function can return an empty string
for slugs made only of separators (e.g., "---" or "_"), so update
titleCaseFromSlug to split on both hyphens and underscores (e.g., split by
/[-_]+/), filter falsy parts, map to capitalized words, and if the resulting
array is empty return the original raw slug instead of "". Change the
implementation referenced by titleCaseFromSlug so InstalledMeta.name never
becomes an empty string when the appId regex allows separator-only values.
- Around line 16-40: In lookupStoreMeta, return the remote Store icon URL as the
InstalledMeta.iconUrl fallback instead of the local
`/setup-api/apps/icon/${appId}` path: when a matching store app is found use
iconUrl: `${STORE_ICONS_BASE}/${match.slug}.png` (or if using fallback when no
match, set iconUrl to that same STORE_ICONS_BASE path using appId/slug), while
keeping name, color, DEFAULT_CATEGORY_COLOR, and CATEGORY_COLORS logic intact;
update references to InstalledMeta and ensure STORE_ICONS_BASE is used so the
client (and InstalledAppIcon) has a remote second source if local icon download
failed.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 9ce3772a-64c1-43dc-b7c8-0333a6cc1f4a

📥 Commits

Reviewing files that changed from the base of the PR and between 41d90d7 and d38a5b8.

📒 Files selected for processing (5)
  • src/app/page.tsx
  • src/app/setup-api/apps/install/route.ts
  • src/app/setup-api/apps/uninstall/route.ts
  • src/components/AppStore.tsx
  • src/lib/store-categories.ts

Comment thread src/app/setup-api/apps/install/route.ts
Comment thread src/app/setup-api/apps/install/route.ts
Comment thread src/app/setup-api/apps/install/route.ts
- titleCaseFromSlug: guard against all-separator slugs ("---" → "")
  by splitting on /[-_]+/ and falling back to raw slug when result is
  empty. Matches the appId validator regex which permits underscores.
- lookupStoreMeta: use remote Store icon URL as meta iconUrl so the
  client <InstalledAppIcon> has a second source when the local icon
  download failed. Matches AppStore.tsx's apiToStoreApp shape so both
  UI and MCP install paths produce identical meta.
- Re-install short-circuit: document the sticky-fallback tradeoff
  inline so future readers don't accidentally re-introduce a refresh
  path without understanding why it was avoided.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/setup-api/apps/install/route.ts`:
- Around line 142-144: The catch that currently only logs configSetMany failures
(inside the install route handling installed_apps/installed_meta updates) must
surface the error to callers: update the catch block around the configSetMany
call to either return a non-2xx response (throw an HttpError or use Response
with status 500) or modify the JSON response to set ok: false and include a
preferenceSyncError field with the error message (use err instanceof Error ?
err.message : String(err)); ensure this change is applied for both catch sites
handling preference sync so callers (MCP/CLI) can detect the failure instead of
receiving ok: true.
- Around line 40-46: Remote categories must be validated before indexing
CATEGORY_COLORS to avoid inherited keys or malicious values being used for
InstalledMeta.color; in the route where you compute color (using data, match,
appId), check that match.category is a non-empty string and that
Object.prototype.hasOwnProperty.call(CATEGORY_COLORS, match.category) (and
optionally that the looked-up value is a valid string/color) before using
CATEGORY_COLORS[match.category]; if the validation fails, use
DEFAULT_CATEGORY_COLOR instead.
- Around line 132-140: When adding an app where alreadyListed is false but
metaMap[appId] already exists, avoid refetching and overwriting that existing
metadata; change the logic in the branch around lookupStoreMeta/configSetMany so
you only call lookupStoreMeta(appId) and set "pref:installed_meta" when
metaMap[appId] is undefined, otherwise only add appId to "pref:installed_apps"
(i.e. build nextUpdates to reuse existing metaMap when present and only include
the fetched storeMeta when missing), then call configSetMany(nextUpdates).
🪄 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: ASSERTIVE

Plan: Pro

Run ID: e5322a2b-2cd7-421c-83c7-dad87e23009d

📥 Commits

Reviewing files that changed from the base of the PR and between d38a5b8 and 7a1f869.

📒 Files selected for processing (1)
  • src/app/setup-api/apps/install/route.ts

Comment thread src/app/setup-api/apps/install/route.ts
Comment thread src/app/setup-api/apps/install/route.ts Outdated
Comment thread src/app/setup-api/apps/install/route.ts
Three fixes on `src/app/setup-api/apps/install/route.ts`:

1. Surface preferenceSyncError to callers (API contract)
   Previously the try/catch around config-store writes only logged the
   failure and still returned ok:true, so MCP/CLI callers couldn't tell
   whether the desktop would actually see the install. Now the response
   carries `preferenceSyncError` and `ok` flips false when it's set.

2. hasOwnProperty guard on CATEGORY_COLORS lookup (hardening)
   Malicious or unexpected `category` value from openclawhardware.dev
   (e.g. '__proto__', 'constructor') would otherwise resolve to an
   inherited property. Validate as string + ownProperty before indexing.

3. Don't overwrite existing meta on partial-state re-install (logic)
   Old branch condition `!alreadyListed || !metaMap[appId]` refetched
   and overwrote meta whenever the list entry was missing, even when
   meta was present. Split into two independent checks: only fetch
   Store meta when meta is absent; only append to list when not listed.

Also update `src/tests/routes/apps/install.test.ts` so the config-store
mock exports getAll/setMany (the route now uses them). 24/24 pass.
@KrasimirKralev

Copy link
Copy Markdown
Contributor Author

Addressed all 3 findings from the second review on 7a1f869: surface preferenceSyncError to callers + flip ok when set, hasOwnProperty guard on CATEGORY_COLORS, don't overwrite existing meta on partial-state re-install. Tests 24/24 pass, live-tested on device. @coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown

@KrasimirKralev Thanks for the detailed update — great to hear all three findings have been addressed and that 24/24 tests are green with live device verification. Triggering a fresh review of the latest commits now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@KrasimirKralev
KrasimirKralev merged commit 08259bb into ID-Robots:beta Apr 19, 2026
4 of 5 checks passed
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.

1 participant