Skip to content

feat(store): Apple App Store-style mobile layout#959

Merged
jaylfc merged 6 commits into
devfrom
feat/mobile-store-appstore
Jun 16, 2026
Merged

feat(store): Apple App Store-style mobile layout#959
jaylfc merged 6 commits into
devfrom
feat/mobile-store-appstore

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 16, 2026

Copy link
Copy Markdown
Owner

What

The Store app rendered its desktop two-pane layout (left section sidebar + content grid) on phones, so text wrapped, cards clipped, and the Get/Preview buttons were cut off. This adds a mobile-only presentation switched on useIsMobile() that reads like the Apple App Store. The desktop render path is left byte-identical (the diff to index.tsx is purely additive: two imports, the hook call, and an early isMobile branch).

Mobile layout

  • Bottom tab bar (Discover / Apps / Agents / Search / Updates) mapped to the existing nav ids, with safe-area-inset-bottom respected.
  • Full-width vertical feed: a featured Editor's Choice hero, then horizontal snap-scroll carousels with peek of the next card, plus App Store-style rows (rounded squircle icon, title, subtitle, trailing Get pill, star count).
  • Sticky header with the section title, a search affordance, and a device-filter chip. Tapping Search opens a full-screen search view.
  • Device selection folds into a header chip + bottom sheet, replacing the desktop device pill bar.

All catalog data, install/uninstall logic, device/backend filters, and the model resolver are reused from index.tsx. Theme tokens only (shell-* / accent / dock), dark graphite default. Mobile components' language (44px bars, frosted dock chrome, active: touch states, safe areas) matches components/mobile/.

Verification

  • npx tsc --noEmit -p tsconfig.json clean.
  • 42 existing Store tests pass (vitest run src/apps/StoreApp).
  • Production npm run build passes.
  • Verified at 390x844 with Playwright across Discover, a carousel mid-scroll, the full-screen Search view, the Apps list (longest title "Code Server (Streamed)" fits, no clipped Get pills), the Updates empty state, and the device chip + bottom sheet. Screenshots saved under .design/research/mobile-store/ (gitignored).

No em dashes. Desktop layout unchanged.

Summary by CodeRabbit

  • New Features
    • Added a mobile-optimized store experience with bottom tabs, sticky section headers, search, device filtering via a bottom sheet, and curated Discover browsing (featured cards, carousel).
    • Installation UI now shows a Get action for not-installed apps and an Installed status indicator when already installed, including a Retry option on install failures.
  • Refactor / UI Improvements
    • Standardized icon and cover rendering with deterministic placeholders; added optional real coverImage support with graceful fallbacks.
  • Tests
    • Added coverage for installed vs not-installed rendering, install success/failure behavior, and resilience when image/icon sources change.

The Store rendered its desktop two-pane layout (left section sidebar +
content grid) on phones, so text wrapped, cards clipped and Get/Preview
buttons were cut off. Add a mobile-only presentation switched on
useIsMobile(), leaving the desktop render path byte-identical.

Mobile layout:
- Bottom tab bar (Discover / Apps / Agents / Search / Updates) mapped to
  the existing nav ids, with safe-area-inset-bottom respected.
- Full-width vertical feed: a featured Editor's Choice hero, then
  horizontal snap-scroll carousels with peek of the next card, plus
  App Store-style rows (rounded icon, title, subtitle, trailing Get pill).
- Sticky header with the section title, a search affordance and a device
  filter chip; tapping Search opens a full-screen search view.
- Device selection folds into a header chip + bottom sheet, replacing the
  desktop device pill bar.

All catalog data, install/uninstall logic, device/backend filters and the
model resolver are reused from index.tsx. Theme tokens only (shell-* /
accent / dock), dark graphite default. tsc clean, 42 Store tests pass.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1bc86952-8718-4585-9893-a5fe7d79fc84

📥 Commits

Reviewing files that changed from the base of the PR and between 28bf552 and 31aba65.

📒 Files selected for processing (7)
  • desktop/public/store-covers/ollama.webp
  • desktop/public/store-covers/stable-diffusion-bw.webp
  • desktop/public/store-covers/stable-diffusion.webp
  • desktop/src/apps/StoreApp/AppIcon.tsx
  • desktop/src/apps/StoreApp/MobileStore.test.tsx
  • desktop/src/apps/StoreApp/MobileStore.tsx
  • desktop/src/apps/StoreApp/index.tsx

📝 Walkthrough

Walkthrough

New MobileStore.tsx (647 lines) implements a mobile App Store UI with tab navigation, device filtering, curated discovery sections (hero, popular, subscriptions, frameworks), search, and install flows. New AppIcon.tsx (289 lines) provides deterministic icon URL resolution with multi-stage fallback and a deterministic monogram fallback using gradient-hashed initials, plus StoreCover component for cover image rendering with gradient fallback. types.ts adds optional coverImage field to CatalogApp. StoreApp/index.tsx is refactored to use the new AppIcon and StoreCover across all desktop cards (AppCard, RichCard, SubscriptionRow, HeroFeatured), removes file-local icon helpers, adds curated cover metadata via COVER_BY_ID, and introduces a useIsMobile() check that early-returns MobileStore on mobile, leaving the desktop sidebar/layout path intact on desktop. Tests verify GetButton install flows, component reuse resilience, and proper state cleanup.

Changes

Mobile Store and Icon Infrastructure

Layer / File(s) Summary
Type updates and shared icon/cover system
desktop/src/apps/StoreApp/types.ts, desktop/src/apps/StoreApp/AppIcon.tsx
Extends CatalogApp with optional coverImage field. Introduces AppIcon component with icon URL resolution (explicit override, CDN slug, family-based fallback), multi-stage onError iteration, and deterministic Monogram fallback using gradient-hashed initials. Exports StoreCover component for cover rendering with image/gradient fallback and text overlay scrims.
Mobile UI presentational components
desktop/src/apps/StoreApp/MobileStore.tsx (lines 1–222)
Defines formatStars and subtitleFor formatters. Implements GetButton with install POST to /api/store/install-v2, busy state, and "Retry" on failure. Adds AppRow/AppRowList for vertical app listings with icon, title, subtitle, stars, and install button. Includes FeatureCard for featured cards with full-bleed cover and optional "Editor's Choice" label. Adds Carousel for horizontally scrollable snap-aligned cards and SectionHead for section titles with optional "See all" action.
Mobile navigation and MobileStore orchestration
desktop/src/apps/StoreApp/MobileStore.tsx (lines 228–647)
Defines MobileTab union, static TABS metadata with labels and lucide icons, and TabBar with active-state styling and aria-current attribute. Implements main MobileStore managing active tab, search query, and device-sheet visibility; derives filtered pools via filterCatalog and compatFromResolver; builds memoized Discover sections (hero, popular, subscriptions, frameworks). Renders sticky header with device chip and search field, conditional Discover vs. tab listing vs. search views, bottom TabBar, and DeviceSheet modal for device-filter selection. Exports SectionView (empty state + list), SearchView (blank/no-results states), and DeviceSheet (multi-select toggles).
Desktop card refactoring and curated cover metadata
desktop/src/apps/StoreApp/index.tsx
Adds imports for useIsMobile, MobileStore, AppIcon, StoreCover. Updates HOMELAB_APPS and MOCK_APPS seed data with coverImage fields. Introduces COVER_BY_ID map to preserve designed cover art when API responses omit metadata. Removes file-local icon helpers (resolveIconUrl, APP_ICONS, di, gh). Refactors AppCard, RichCard, SubscriptionRow, and HeroFeatured to use shared AppIcon and StoreCover instead of per-card icon rendering and per-card iconFailed state.
Mobile detection and StoreApp routing
desktop/src/apps/StoreApp/index.tsx (lines 829, 879–880, 1034–1052)
Adds useIsMobile() hook to derive isMobile state. Updates catalog hydration to wire cover and coverImage from API values or COVER_BY_ID fallback. Introduces early-return: when isMobile is true, renders MobileStore wired with catalog state and handleInstall, bypassing desktop sidebar/layout entirely.
MobileStore component and integration tests
desktop/src/apps/StoreApp/MobileStore.test.tsx
Tests GetButton rendering for installed vs. not-installed apps, install flow success with onInstall callback, and install flow failure with "Retry" affordance. Validates StoreCover and AppIcon component reuse resilience—both correctly reset internal state when app props change on reused instances. Includes jsdom scrollTo polyfill and proper test cleanup.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant StoreApp
  participant MobileStore
  participant TabBar
  participant FilterCatalog
  participant DeviceSheet
  participant GetButton
  participant InstallAPI as /api/store/install-v2

  User->>StoreApp: load store
  StoreApp->>StoreApp: check useIsMobile()
  alt isMobile = true
    StoreApp->>MobileStore: render with catalog state
  else isMobile = false
    StoreApp->>StoreApp: render desktop sidebar layout
  end

  User->>TabBar: tap tab (Discover/Apps/etc.)
  TabBar->>MobileStore: setTab(tab), scroll reset
  MobileStore->>FilterCatalog: filter apps by tab + devices/backends
  FilterCatalog-->>MobileStore: filtered app pool
  MobileStore->>User: render Discover sections or SectionView

  User->>MobileStore: tap device filter chip
  MobileStore->>DeviceSheet: open sheet
  DeviceSheet->>User: render device toggle list
  User->>DeviceSheet: toggle device, tap Done
  DeviceSheet->>MobileStore: updated selectedDevices Set
  MobileStore->>FilterCatalog: re-filter with new devices

  User->>GetButton: tap "Get"
  GetButton->>GetButton: set busy=true
  GetButton->>InstallAPI: POST {slug, targets}
  InstallAPI-->>GetButton: 200 OK
  GetButton->>MobileStore: onInstall(app.id)
  GetButton->>GetButton: show "Open" state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jaylfc/taOS#871: Introduces catalog metadata fields (iconSlug, stars, cover) that MobileStore's icon resolution, star formatting, and FeatureCard cover rendering directly depend on.
  • jaylfc/taOS#561: Both PRs modify StoreApp icon sourcing logic—the retrieved PR vendors Simple Icons locally via si, while this PR removes old icon-resolution helpers and centralizes icon handling in the new AppIcon component.

Poem

🐇 Hippity-hop, a mobile store blooms,
With tabs at the bottom and carousel rooms!
Icons cascade through fallbacks so graceful,
From CDN to monogram—a gradient masterpiece.
Swipe, filter, and tap "Get"—the spinner spins round,
A pocket-sized storefront has finally been found! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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: introducing a mobile-optimized Apple App Store-style layout for the Store app, which is the primary feature added across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 82.14% which is sufficient. The required threshold is 80.00%.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mobile-store-appstore

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.

Comment thread desktop/src/apps/StoreApp/MobileStore.tsx
Comment thread desktop/src/apps/StoreApp/MobileStore.tsx
Comment thread desktop/src/apps/StoreApp/MobileStore.tsx

@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

🤖 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 `@desktop/src/apps/StoreApp/index.tsx`:
- Around line 1092-1100: The MobileStore component is being passed the
selectedBackends prop which contains persisted backend filters, but the mobile
UI provides no way to change or clear these filters, causing apps to be silently
hidden. Either remove the selectedBackends prop from the MobileStore component
invocation and pass an empty array instead, or add a handler callback (similar
to onDevicesChange for devices) that allows the mobile UI to modify and clear
backend filters. The safest short-term approach is to reset selectedBackends to
an empty array when rendering MobileStore so the filter does not persist on
mobile.

In `@desktop/src/apps/StoreApp/MobileStore.tsx`:
- Around line 101-123: The GetButton component currently always uses the first
install target from the installTargets array instead of respecting the user's
selection. Add a new prop to the GetButton function (such as selectedTarget or
activeTarget) that receives the currently selected install target from the
parent component, and use this prop instead of installTargets[0] when
constructing the fetch request body at the line setting target_remote. Then
update all call sites where GetButton is instantiated to pass the currently
selected target as this new prop, ensuring the user's device selection is
properly propagated through all usage paths.
🪄 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: b46cb4cd-9a14-44da-91ea-068b08639f71

📥 Commits

Reviewing files that changed from the base of the PR and between ac0aad2 and 942a019.

📒 Files selected for processing (2)
  • desktop/src/apps/StoreApp/MobileStore.tsx
  • desktop/src/apps/StoreApp/index.tsx

Comment on lines +1092 to +1100
<MobileStore
apps={apps}
loading={loading}
installTargets={installTargets}
selectedDevices={selectedDevices}
onDevicesChange={setSelectedDevices}
selectedBackends={selectedBackends}
compatMap={compatMap}
onInstall={handleInstall}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mobile path applies backend filters without any mobile control to change them.

This branch passes persisted selectedBackends into MobileStore, but the mobile UI has no backend picker/clear action. A previously saved backend filter can silently hide apps with no recovery path.

Safe short-term mitigation
       <MobileStore
         apps={apps}
         loading={loading}
         installTargets={installTargets}
         selectedDevices={selectedDevices}
         onDevicesChange={setSelectedDevices}
-        selectedBackends={selectedBackends}
+        selectedBackends={[]}
         compatMap={compatMap}
         onInstall={handleInstall}
       />
📝 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
<MobileStore
apps={apps}
loading={loading}
installTargets={installTargets}
selectedDevices={selectedDevices}
onDevicesChange={setSelectedDevices}
selectedBackends={selectedBackends}
compatMap={compatMap}
onInstall={handleInstall}
<MobileStore
apps={apps}
loading={loading}
installTargets={installTargets}
selectedDevices={selectedDevices}
onDevicesChange={setSelectedDevices}
selectedBackends={[]}
compatMap={compatMap}
onInstall={handleInstall}
🤖 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 `@desktop/src/apps/StoreApp/index.tsx` around lines 1092 - 1100, The
MobileStore component is being passed the selectedBackends prop which contains
persisted backend filters, but the mobile UI provides no way to change or clear
these filters, causing apps to be silently hidden. Either remove the
selectedBackends prop from the MobileStore component invocation and pass an
empty array instead, or add a handler callback (similar to onDevicesChange for
devices) that allows the mobile UI to modify and clear backend filters. The
safest short-term approach is to reset selectedBackends to an empty array when
rendering MobileStore so the filter does not persist on mobile.

Comment on lines +101 to +123
function GetButton({
app, onInstall, installTargets,
}: {
app: CatalogApp;
onInstall: (id: string) => void;
installTargets: InstallTarget[];
}) {
const [busy, setBusy] = useState(false);

const handleGet = useCallback(async () => {
if (app.installed || busy) return;
setBusy(true);
try {
const target = installTargets[0]?.name ?? "local";
const res = await fetch("/api/store/install-v2", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ app_id: app.id, target_remote: target }),
});
if (res.ok) onInstall(app.id);
} catch { /* network blip - leave as Get */ }
setBusy(false);
}, [app.id, app.installed, busy, installTargets, onInstall]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Install target selection ignores the device the user picked.

At Line 114, the request always uses installTargets[0], so mobile installs can go to the wrong host even after selecting a device in the sheet.

Proposed fix
 function GetButton({
-  app, onInstall, installTargets,
+  app, onInstall, installTargets, selectedDevices,
 }: {
   app: CatalogApp;
   onInstall: (id: string) => void;
   installTargets: InstallTarget[];
+  selectedDevices: string[];
 }) {
@@
-      const target = installTargets[0]?.name ?? "local";
+      const target =
+        selectedDevices.length === 1
+          ? selectedDevices[0]
+          : (installTargets[0]?.name ?? "local");
       const res = await fetch("/api/store/install-v2", {
@@
-  }, [app.id, app.installed, busy, installTargets, onInstall]);
+  }, [app.id, app.installed, busy, installTargets, onInstall, selectedDevices]);
-      <GetButton app={app} onInstall={onInstall} installTargets={installTargets} />
+      <GetButton app={app} onInstall={onInstall} installTargets={installTargets} selectedDevices={selectedDevices} />

(Apply the same prop pass-through for each GetButton usage path.)

🤖 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 `@desktop/src/apps/StoreApp/MobileStore.tsx` around lines 101 - 123, The
GetButton component currently always uses the first install target from the
installTargets array instead of respecting the user's selection. Add a new prop
to the GetButton function (such as selectedTarget or activeTarget) that receives
the currently selected install target from the parent component, and use this
prop instead of installTargets[0] when constructing the fetch request body at
the line setting target_remote. Then update all call sites where GetButton is
instantiated to pass the currently selected target as this new prop, ensuring
the user's device selection is properly propagated through all usage paths.

const compatibleFor = useCallback((pool: CatalogApp[]): CatalogApp[] => {
const selDevObjs = installTargets.filter((t) => selectedDevices.includes(t.name));
const { compatible } = filterCatalog(pool, selDevObjs, selectedBackends);
return compatible.filter((a) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Incompatible apps are hidden without an affordance

compatibleFor discards filterCatalog(...).incompatible and MobileStore never renders an IncompatibleToggle, unlike the desktop grid. Users can see empty or shortened lists with no indication that apps were filtered out by device/backend/model-resolver constraints. Add a mobile incompatible toggle/banner or show filtered-out counts so empty states are explainable.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

onChange(selSet.has(name) ? selected.filter((n) => n !== name) : [...selected, name]);
};
return (
<div className="absolute inset-0 z-50 flex flex-col justify-end" role="dialog" aria-modal="true" aria-label="Filter by device">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Device sheet lacks dialog focus management

The sheet declares role="dialog" but does not move focus into it, trap Tab focus, return focus to the device chip, or close on Escape. Keyboard and screen-reader users may lose context or tab into background content. Add focus trapping/focus restoration and Escape handling, or use an existing dialog primitive if available.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// snap-scroll carousels and a full-screen search. Same data and install
// handlers as desktop; only the presentation changes. The desktop render
// path below is left untouched.
if (isMobile) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Mobile branch still pays desktop filtering cost

StoreApp computes searchFiltered, navFiltered, filterCatalog, availableBackends, and related effects before the isMobile early return. On mobile renders this does unnecessary work for desktop-only grid state. Move the mobile branch earlier where possible or memoize/split the expensive desktop-only calculations to avoid wasted work on the mobile path.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 2
Issue Details (click to expand)

CRITICAL

No CRITICAL issues found.

WARNING

File Line Issue
desktop/src/apps/StoreApp/MobileStore.tsx 58 Mobile installs ignore the selected device target; GetButton uses installTargets[0] instead of selectedDevices, so installs can target the wrong host.
desktop/src/apps/StoreApp/MobileStore.tsx 306 Incompatible apps are filtered out but never exposed through a mobile toggle/banner, so users can get unexplained empty or shortened lists.
desktop/src/apps/StoreApp/index.tsx 246 Stable Diffusion fallback cover keys do not match catalog IDs; the catalog has stable-diffusion-cpp, but this map keys sd-webui, leaving stable-diffusion-cpp without a fallback cover and making sd-webui unused.

SUGGESTION

File Line Issue
desktop/src/apps/StoreApp/MobileStore.tsx 585 Device sheet uses role="dialog" without focus management, focus restoration, or Escape-to-close handling.
desktop/src/apps/StoreApp/index.tsx 1038 Mobile rendering still computes desktop-only filtering and backend state before the early return, causing avoidable render work.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
None
Files Reviewed (2 files)
  • desktop/src/apps/StoreApp/MobileStore.tsx - 3 issues
  • desktop/src/apps/StoreApp/index.tsx - 2 issues

Fix these issues in Kilo Cloud

Previous Review Summary (commit 65804ad)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 65804ad)

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 2
Issue Details (click to expand)

CRITICAL

No CRITICAL issues found.

WARNING

File Line Issue
desktop/src/apps/StoreApp/MobileStore.tsx 58 Mobile installs ignore the selected device target; GetButton uses installTargets[0] instead of selectedDevices, so installs can target the wrong host.
desktop/src/apps/StoreApp/MobileStore.tsx 306 Incompatible apps are filtered out but never exposed through a mobile toggle/banner, so users can get unexplained empty or shortened lists.
desktop/src/apps/StoreApp/index.tsx 246 Stable Diffusion fallback cover keys do not match catalog IDs; the catalog has stable-diffusion-cpp, but this map keys sd-webui, leaving stable-diffusion-cpp without a fallback cover and making sd-webui unused.

SUGGESTION

File Line Issue
desktop/src/apps/StoreApp/MobileStore.tsx 585 Device sheet uses role="dialog" without focus management, focus restoration, or Escape-to-close handling.
desktop/src/apps/StoreApp/index.tsx 1038 Mobile rendering still computes desktop-only filtering and backend state before the early return, causing avoidable render work.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
None
Files Reviewed (2 files)
  • desktop/src/apps/StoreApp/MobileStore.tsx - 3 issues
  • desktop/src/apps/StoreApp/index.tsx - 2 issues

Fix these issues in Kilo Cloud


Reviewed by nex-n2-pro:free · 883,136 tokens

jaylfc added a commit that referenced this pull request Jun 16, 2026
Every Store app now shows an intentional icon instead of a blank tile.
A new shared AppIcon component (desktop + mobile) resolves the logo
through dashboard-icons, tries a name-derived slug, then falls back to
a branded monogram: the app initials on a deterministic per-app
gradient tuned for the graphite shell. The taOS agent frameworks
(OpenClaw, Hermes, IronClaw, MicroClaw, Moltis, Agent Zero, Langroid),
which have no upstream logo, now read as clean monograms.

Real Discover logos are wired via verified dashboard-icons slugs
(comfyui, n8n, ollama, qwen, gemma, github, mcp, hugging-face, openai
and the homelab set). The dead /static/store-icons SVG and .jpg
references that silently 404'd are gone.

A coverFor helper gives featured and carousel cards a distinct cover:
an explicit app.cover when set, otherwise a layered gradient from the
same hue family as the icon, so the hero reads like an App Store
featured card and no card ships flat.

AppIcon and coverFor replace the duplicated icon resolvers and inline
Package fallbacks across MobileStore and the desktop StoreApp.
Comment thread desktop/src/apps/StoreApp/AppIcon.tsx

@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.

Caution

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

⚠️ Outside diff range comments (1)
desktop/src/apps/StoreApp/MobileStore.tsx (1)

333-333: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Potential crash if description is undefined.

If any CatalogApp has an undefined or null description, calling .toLowerCase() will throw a TypeError, crashing the search functionality.

🛡️ Proposed fix with defensive access
     return compatibleFor(
-      apps.filter((a) => a.name.toLowerCase().includes(q) || a.description.toLowerCase().includes(q)),
+      apps.filter((a) => a.name.toLowerCase().includes(q) || (a.description ?? "").toLowerCase().includes(q)),
     );
🤖 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 `@desktop/src/apps/StoreApp/MobileStore.tsx` at line 333, The filter function
in the apps list is calling .toLowerCase() on a.description without checking if
it exists first, which will crash if any CatalogApp has an undefined or null
description. Add a defensive check using optional chaining or a null coalescing
operator to safely handle cases where description is undefined or null. Either
use optional chaining like a.description?.toLowerCase().includes(q) or provide a
fallback empty string like (a.description ?? "").toLowerCase().includes(q) so
the search continues working even when description is missing.
♻️ Duplicate comments (1)
desktop/src/apps/StoreApp/index.tsx (1)

1010-1023: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mobile path receives backend filters without a mobile UI to change them.

Line 1018 passes selectedBackends to MobileStore, but the mobile UI (per MobileStore.tsx) provides no backend picker. Users on mobile inherit persisted backend filters with no recovery path to clear or change them, potentially hiding compatible apps.

Short-term mitigation
       <MobileStore
         apps={apps}
         loading={loading}
         installTargets={installTargets}
         selectedDevices={selectedDevices}
         onDevicesChange={setSelectedDevices}
-        selectedBackends={selectedBackends}
+        selectedBackends={[]}
         compatMap={compatMap}
         onInstall={handleInstall}
       />
🤖 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 `@desktop/src/apps/StoreApp/index.tsx` around lines 1010 - 1023, The
MobileStore component is receiving the selectedBackends prop but lacks any UI
controls for users to modify backend filters on mobile, trapping them with
persisted filters they cannot change. Remove the selectedBackends prop from the
MobileStore component call to prevent mobile users from being locked into
backend filters they have no way to adjust, ensuring they can see all compatible
apps regardless of previously selected backend filters.
🧹 Nitpick comments (1)
desktop/src/apps/StoreApp/AppIcon.tsx (1)

74-78: 💤 Low value

Consider simplifying the redundant null-coalescing.

Line 76 checks if (APP_ICONS[app.id]) then returns APP_ICONS[app.id] ?? null. Since all APP_ICONS values are strings, the if-check and ?? null are redundant.

♻️ Simpler alternative
 function primaryIconUrl(app: CatalogApp): string | null {
   if (app.iconSlug) return di(app.iconSlug);
-  if (APP_ICONS[app.id]) return APP_ICONS[app.id] ?? null;
-  return familyIcon(app.id);
+  return APP_ICONS[app.id] ?? familyIcon(app.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 `@desktop/src/apps/StoreApp/AppIcon.tsx` around lines 74 - 78, The
primaryIconUrl function contains a redundant null-coalescing pattern on line 76.
The if-check already ensures APP_ICONS[app.id] is truthy, making the subsequent
?? null operator redundant. Simplify the return statement in the APP_ICONS
condition to just return APP_ICONS[app.id] directly without the null-coalescing
operator, since the truthiness check already guarantees a non-null value.
🤖 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.

Outside diff comments:
In `@desktop/src/apps/StoreApp/MobileStore.tsx`:
- Line 333: The filter function in the apps list is calling .toLowerCase() on
a.description without checking if it exists first, which will crash if any
CatalogApp has an undefined or null description. Add a defensive check using
optional chaining or a null coalescing operator to safely handle cases where
description is undefined or null. Either use optional chaining like
a.description?.toLowerCase().includes(q) or provide a fallback empty string like
(a.description ?? "").toLowerCase().includes(q) so the search continues working
even when description is missing.

---

Duplicate comments:
In `@desktop/src/apps/StoreApp/index.tsx`:
- Around line 1010-1023: The MobileStore component is receiving the
selectedBackends prop but lacks any UI controls for users to modify backend
filters on mobile, trapping them with persisted filters they cannot change.
Remove the selectedBackends prop from the MobileStore component call to prevent
mobile users from being locked into backend filters they have no way to adjust,
ensuring they can see all compatible apps regardless of previously selected
backend filters.

---

Nitpick comments:
In `@desktop/src/apps/StoreApp/AppIcon.tsx`:
- Around line 74-78: The primaryIconUrl function contains a redundant
null-coalescing pattern on line 76. The if-check already ensures
APP_ICONS[app.id] is truthy, making the subsequent ?? null operator redundant.
Simplify the return statement in the APP_ICONS condition to just return
APP_ICONS[app.id] directly without the null-coalescing operator, since the
truthiness check already guarantees a non-null value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 047eab20-cca6-40bb-b316-5889155a0466

📥 Commits

Reviewing files that changed from the base of the PR and between 942a019 and 2463283.

📒 Files selected for processing (3)
  • desktop/src/apps/StoreApp/AppIcon.tsx
  • desktop/src/apps/StoreApp/MobileStore.tsx
  • desktop/src/apps/StoreApp/index.tsx

Official screenshots/hero art for the real apps (comfyui, n8n, home-assistant,
immich, jellyfin, sonarr, radarr, uptime-kuma, vaultwarden, code-server,
nextcloud) saved as optimized webp under public/store-covers and shown behind
the featured + carousel cards via a new coverImage field (falls back to the
gradient when absent). taOS-specific frameworks keep the branded monogram cover.
Comment thread desktop/src/apps/StoreApp/AppIcon.tsx
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ app_id: app.id, target_remote: target }),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Mobile installs ignore the selected device target

GetButton always posts target_remote using installTargets[0], even though the mobile device sheet lets users select a target device and StoreApp passes selectedDevices into MobileStore. With multiple install targets, an install can be sent to the wrong host after the user changes the device selection.

Derive the target from selectedDevices (for example selectedDevices[0] ?? installTargets[0]?.name ?? "local") and thread that value through each GetButton call site.

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

jaylfc added 2 commits June 16, 2026 17:06
Official banners (no upstream dashboard-icons logo for these taOS frameworks),
converted to webp and keyed by id in COVER_BY_ID so they resolve for the curated
and backend-sourced rows alike.
Add Ollama banner and a shared Stable Diffusion banner across both
WebUI cards. The AUTOMATIC1111 build (sd-webui) uses a grayscale cut of
the same banner so the two Stable Diffusion entries read as distinct.
Covers resolve via COVER_BY_ID by app id for backend-sourced catalog rows.
jaylfc added a commit that referenced this pull request Jun 16, 2026
// Both Stable Diffusion WebUI cards share one banner; the AUTOMATIC1111
// build (sd-webui) gets a grayscale cut so the two read as distinct.
"stable-diffusion-webui": { coverImage: "/desktop/store-covers/stable-diffusion.webp" },
"sd-webui": { coverImage: "/desktop/store-covers/stable-diffusion-bw.webp" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Stable Diffusion fallback cover keys do not match catalog IDs

app-catalog/catalog.yaml:626 has stable-diffusion-webui and stable-diffusion-cpp, but this map uses stable-diffusion-webui and sd-webui. As a result, stable-diffusion-cpp gets no fallback cover and the sd-webui entry appears unused. Confirm the intended variant mapping and key the fallbacks against the actual catalog IDs.

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

- Installed apps no longer render a dead Open span: show an honest
  role=status Installed indicator (store services are managed, not
  launched from the storefront), removing the misleading affordance.
- Surface install failures: a non-ok or failed request now flips the
  pill to a Retry state instead of silently swallowing the error.
- AppIcon resets its resolution stage when the candidate URL set changes,
  so a reused instance does not carry a stale error stage to a new app.
- StoreCover clears its failed flag when coverImage changes, so a reused
  instance retries the new app's cover.
- Add MobileStore tests covering Get/Installed/Retry states and the
  AppIcon/StoreCover instance-reuse resets (47 Store tests pass).
@jaylfc jaylfc merged commit 6f6fda5 into dev Jun 16, 2026
7 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jun 16, 2026
@gitar-bot

gitar-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 5 resolved / 5 findings

Implements an Apple App Store-style mobile layout for the store, resolving issues with action buttons, error handling, component re-usability, and testing. No open findings remain.

✅ 5 resolved
Bug: Installed apps have no working action on mobile (Open is a dead span)

📄 desktop/src/apps/StoreApp/MobileStore.tsx:125-131 📄 desktop/src/apps/StoreApp/MobileStore.tsx:150-164 📄 desktop/src/apps/StoreApp/index.tsx:1003-1006 📄 desktop/src/apps/StoreApp/index.tsx:1090-1102
For installed apps GetButton renders a non-interactive <span> reading "Open" (MobileStore.tsx:125-131). It has no onClick, so tapping it does nothing - the label promises an action it does not perform. More importantly, the desktop AppCard exposes an Uninstall action for installed apps (index.tsx:515-523, with handleUninstall), but MobileStore is never passed handleUninstall (index.tsx:1090-1102) and provides no uninstall affordance anywhere. As a result mobile users cannot uninstall apps at all, and the "Open" pill is misleading. Consider either wiring "Open" to an actual launch/app-open action, or surfacing the uninstall path (e.g. pass onUninstall and show it on the installed row/sheet).

Quality: Install failures are silently swallowed with no user feedback

📄 desktop/src/apps/StoreApp/MobileStore.tsx:110-123
In GetButton.handleGet a non-ok response (if (res.ok) onInstall(...)) and any network error (catch { /* network blip */ }) are silently ignored: busy is reset and the button simply returns to "Get" (MobileStore.tsx:110-123). The user gets no indication that the install failed and may tap repeatedly assuming nothing happened. Consider surfacing an error state (toast, inline message, or transient error styling) on failure so the action is not silently lost.

Quality: New 704-line MobileStore component ships without tests

📄 desktop/src/apps/StoreApp/MobileStore.tsx:332-346
MobileStore.tsx adds substantial new presentation and filtering logic (compatibleFor, tabPool/NAV_TYPE_MAP routing, search filtering, hero/popular/subscriptions/frameworks curation, device sheet) but no accompanying tests were added; the PR notes only that the 42 existing Store tests still pass. Logic such as compatibleFor, the updates filter, and the tab type mapping is easy to regress. Consider adding unit/component tests covering tab switching, search results, the empty states, and device-filter behavior.

Edge Case: AppIcon stage not reset when app prop changes on reused instance

📄 desktop/src/apps/StoreApp/AppIcon.tsx:186-199
In AppIcon, stage is component state initialized to 0 and only ever advanced via onError. The candidates list is recomputed via useMemo([app]) when app changes, but stage is not reset. If React reuses a mounted AppIcon instance for a different app (e.g., a list re-orders/filters and items are keyed by index rather than app.id, or the same DOM position renders a different app), a carried-over stage can cause the new app to skip its primary icon URL or render the monogram immediately (when stage >= candidates.length from the previous app). The Store lists are filtered/re-sorted by device/backend selection, so this is reachable.

Suggested fix: reset stage whenever the candidate set changes, e.g. with an effect useEffect(() => setStage(0), [candidates]) (or key the component by app.id at every call site, and/or include app.id in the memo dependency to make the reset deterministic). This guarantees each app starts its icon resolution from stage 0.

Edge Case: StoreCover failed state not reset when app prop changes

📄 desktop/src/apps/StoreApp/AppIcon.tsx:182-196
StoreCover tracks image-load failure in component-local useState(false) and never resets it when the app prop changes. If React reuses a StoreCover instance for a different app (e.g. a parent that renders a card without keying by app.id, or a list reorder/virtualization), a previously-failed image leaves failed === true, so a new app with a perfectly valid coverImage is never attempted and only its gradient fallback shows. This is the same class of issue already flagged for AppIcon's stage state. In the current call sites cards are keyed by app.id, so the impact is limited today, but the component is exported and reusable. Reset the failure flag whenever the image source changes, e.g. with useEffect(() => setFailed(false), [app.coverImage]) or by giving the <img> a key={app.coverImage}.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

jaylfc added a commit that referenced this pull request Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant