Skip to content

feat(apps): per-app versioning + Updates UI surface (#89 P1)#967

Merged
jaylfc merged 2 commits into
devfrom
feat/app-versioning-updates-ui
Jun 16, 2026
Merged

feat(apps): per-app versioning + Updates UI surface (#89 P1)#967
jaylfc merged 2 commits into
devfrom
feat/app-versioning-updates-ui

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Phase 1 of userspace app packages (#89, Track A "rails first"): make optional-app versions real and visible, structured for future independent per-app updates. No package format or dynamic code loading yet.

Backend (routes/apps.py)

  • APP_VERSIONS + APP_TRUST maps: the source of truth for in-core optional apps (a real .taosapp package version wins later).
  • GET /api/apps/optional/catalog returns per-app {id, version, trust, source: "core", installed, update_available}; allowlist-bounded; semver-aware update_available.
  • install_optional_app now records the app version.

Frontend

  • Settings/Updates (authoritative): new "Apps" section under the taOS core row. Each installed app shows its version + a Core badge. Rows are keyed by source, so a future "package" source gets its own independent Update button with no layout rework. A Core app's "update" routes to the system update (honest: in-core code updates with the OS).
  • Store/Updates (mirror): updatable optional apps surface in the Updates tab.

Tests

Backend: catalog shape, install records version, update_available flips on an older install row, allowlist isolation (5). Frontend: UpdatesPanel rows + empty state (3), Store updates tab (3). tsc clean, build green, 17 backend + 10 vitest pass locally.

Honest semantics

While the studios are in-core, their update path IS the system update. P1 surfaces versions + the row structure; it does not fake independent updates. Independent per-app updates land when studios become real .taosapp packages in later phases.

Part of #89.

Summary by CodeRabbit

  • New Features
    • Added support for optional in-bundle apps to be tracked and surfaced in Settings’ Updates panel, including version details and update status.
    • Installed optional apps now show “Core”/included indicators and an “Install now” action that jumps to the system update section.
    • Store app “updates” view now highlights optional apps with available updates and adjusts the empty-state messaging accordingly.
  • Tests
    • Added/extended coverage for optional app catalog behavior and UI rendering for update-available vs up-to-date cases.

@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

📝 Walkthrough

Walkthrough

Adds a GET /api/apps/optional/catalog backend endpoint with APP_VERSIONS, APP_TRUST dicts and a _semver_tuple() helper to compute update_available per allowlisted optional app. The install flow records app version. Two frontend components (UpdatesPanel, StoreApp) fetch the catalog and render optional-app update states. New backend and frontend tests cover the full flow.

Changes

Optional App Catalog Feature

Layer / File(s) Summary
Backend catalog: data structures, semver helper, and endpoint
tinyagentos/routes/apps.py
Adds APP_VERSIONS and APP_TRUST dicts, _semver_tuple() for semver ordering and comparison, and GET /api/apps/optional/catalog that returns version, trust, installed, and update_available per allowlisted app. install_optional_app now records version=APP_VERSIONS.get(app_id, "1.0.0") in the store.
UpdatesPanel: optional app row component and catalog integration
desktop/src/apps/SettingsApp/UpdatesPanel.tsx
Introduces OptionalAppCatalogEntry type, AppIconGlyph dynamic-icon helper (with Package fallback), and OptionalAppRow rendering version/Core badge/Install-now scroll link. Adds optionalCatalog state, systemCardRef, mount-time fetch of /api/apps/optional/catalog, scrollToSystem callback, and an "Apps" render section.
StoreApp: optional catalog fetch and conditional updates UI
desktop/src/apps/StoreApp/index.tsx
Adds optionalCatalog state populated from /api/apps/optional/catalog on mount. Replaces the static "You're all up to date" empty state with conditional logic: renders a "taOS Apps" updatable list when installed optional apps have update_available, otherwise shows the original card.
Backend and frontend test coverage for catalog and update UI
tests/test_apps_installed.py, desktop/src/apps/SettingsApp/UpdatesPanel.test.tsx, desktop/src/apps/StoreApp/updates-optional.test.tsx
Backend TestOptionalAppCatalog validates allowlist completeness, recorded version in install, update_available logic for fresh and older versions, and no-leakage of non-allowlisted apps. UpdatesPanel tests centralize fetch mocking with BASE_FETCH helper and add optional apps section suite. StoreApp gets new updates-optional suite covering mount-time catalog fetch, empty-state behavior, and update-available rendering.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jaylfc/taOS#946: Adds the optional app install/uninstall API and stores kind=frontend-app metadata — this PR extends that foundation by recording a version on install and adding the /api/apps/optional/catalog endpoint and frontend update UI on top of it.
  • jaylfc/taOS#871: Extends the StoreApp catalog/update model by adding update_available and catalog fields that this PR then uses to drive the optional-app updates listing.

Poem

🐰 Hop hop, a catalog's born today,
Versions tracked in a tidy array,
update_available? Oh yes, it's true!
The bunny checked semver and flagged what's new.
Core badge shining, scroll to install—hooray! 🎉

🚥 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 PR title accurately summarizes the main change: implementing per-app versioning and exposing it through Updates UI surfaces, which is the core objective across backend and frontend changes.
Docstring Coverage ✅ Passed Docstring coverage is 94.12% 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/app-versioning-updates-ui

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/index.tsx Outdated
Comment on lines +1159 to +1173
) : activeNav === "updates" && filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center h-40 text-shell-text-tertiary text-sm gap-2">
<Package className="w-8 h-8" />
<span>You&rsquo;re all up to date</span>
</div>
<>
{(() => {
const updatableOptional = optionalCatalog.filter((e) => e.installed && e.update_available);
return updatableOptional.length > 0 ? (
<div className="mb-4">
<h3 className="text-[13px] font-semibold text-shell-text mb-2">taOS Apps</h3>
<div className="flex flex-col gap-1">
{updatableOptional.map((entry) => {
const meta = OPTIONAL_APPS.find((a) => a.id === entry.id);
return (
<div key={entry.id} className="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-shell-surface border border-shell-border">
<span className="text-[12.5px] font-medium text-shell-text">{meta?.name ?? entry.id}</span>
<span className="font-mono text-[10px] text-shell-text-tertiary">{entry.version}</span>
<span className="text-[9px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded bg-shell-surface-active text-shell-text-tertiary border border-shell-border-strong">Core</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Optional app updates hidden when other store updates exist

In StoreApp/index.tsx the updatable optional apps (taOS Apps section) are rendered only inside the branch activeNav === "updates" && filtered.length === 0. That branch is taken exclusively when there are no other updatable framework/store apps. As soon as filtered.length > 0 (i.e. there is at least one regular store/framework update), control falls through to the filtered.map(...) grid at the bottom, and the optional-app updates are never displayed.

This means a user who has both a framework update and an updatable optional (core) app will only see the framework update — the optional-app update silently disappears from the Updates tab. The catalog is fetched correctly, but the surface only appears in the empty state.

Suggested fix: render the updatableOptional section above the regular updates grid regardless of filtered.length, rather than nesting it inside the filtered.length === 0 empty-state branch.

Was this helpful? React with 👍 / 👎

Comment on lines +298 to +299
// Only show installed optional apps that have a matching registry entry for display metadata.
const installedOptional = optionalCatalog.filter((e) => e.installed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Studio apps render raw ids/generic icons; comment claims filtering

The backend optional_app_catalog returns all 9 allowlisted ids, including the five studios (coding-studio, design-studio, music-studio, app-studio, office-suite). However the frontend display registry OPTIONAL_APPS only contains 4 entries (reddit, youtube-library, github-browser, x-monitor).

In UpdatesPanel.tsx, the comment on line ~298 states: "Only show installed optional apps that have a matching registry entry for display metadata", but the actual code does not filter on registry membership — const installedOptional = optionalCatalog.filter((e) => e.installed). As a result an installed studio renders with its raw id as the display name (meta?.name ?? entry.id → e.g. "coding-studio") and the generic package icon (meta?.icon ?? "package"). The same mismatch exists in StoreApp/index.tsx's updates section (meta?.name ?? entry.id).

Suggested fix: either add the studio entries to the OPTIONAL_APPS registry so they have proper name/icon metadata, or actually filter the catalog to ids present in the registry (and make the comment truthful). Pick one consistently across both UpdatesPanel and StoreApp.

Was this helpful? React with 👍 / 👎

Comment thread tinyagentos/routes/apps.py Outdated
Comment on lines +65 to +75
def _semver_tuple(version: str) -> tuple[int, ...]:
"""Parse a semver string into a comparable tuple of ints.

Strips leading 'v' and ignores pre-release/build suffixes for ordering.
Returns (0,) on any parse failure so comparisons degrade gracefully.
"""
v = version.lstrip("v").split("-")[0].split("+")[0]
try:
return tuple(int(p) for p in v.split("."))
except ValueError:
return (0,)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: _semver_tuple mismatched-length comparison can falsely flag updates

_semver_tuple parses versions into variable-length tuples. Comparing tuples of unequal length can produce surprising results: e.g. a recorded version "1.0" becomes (1, 0) and the current "1.0.0" becomes (1, 0, 0); since (1, 0) < (1, 0, 0) is True, update_available would falsely flip true even though the versions are semantically equal. Also, a parse failure returns (0,), which compared against a valid (0, 9, 0) yields (0, 9, 0) < (0,) → False, masking a legitimate update.

Currently all APP_VERSIONS are uniform "1.0.0" so this is latent, but it will surface once versions diverge in length. Suggested fix: normalize both tuples to a fixed length (pad with zeros) before comparing, e.g. def _semver_tuple(v): parts = ...; return tuple((parts + (0,0,0))[:3]).

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

Note

Your trial team has used its Gitar budget, so automatic reviews are paused. Upgrade now to unlock full capacity. Comment "Gitar review" to trigger a review manually.
Learn more about usage limits

Code Review ⚠️ Changes requested 0 resolved / 3 findings

Implements per-app versioning and UI updates for system apps, but hides optional app updates when other store items exist, incorrectly displays raw studio IDs, and contains a semver comparison logic bug.

⚠️ Bug: Optional app updates hidden when other store updates exist

📄 desktop/src/apps/StoreApp/index.tsx:1159-1173 📄 desktop/src/apps/StoreApp/index.tsx:1202-1216

In StoreApp/index.tsx the updatable optional apps (taOS Apps section) are rendered only inside the branch activeNav === "updates" && filtered.length === 0. That branch is taken exclusively when there are no other updatable framework/store apps. As soon as filtered.length > 0 (i.e. there is at least one regular store/framework update), control falls through to the filtered.map(...) grid at the bottom, and the optional-app updates are never displayed.

This means a user who has both a framework update and an updatable optional (core) app will only see the framework update — the optional-app update silently disappears from the Updates tab. The catalog is fetched correctly, but the surface only appears in the empty state.

Suggested fix: render the updatableOptional section above the regular updates grid regardless of filtered.length, rather than nesting it inside the filtered.length === 0 empty-state branch.

⚠️ Bug: Studio apps render raw ids/generic icons; comment claims filtering

📄 desktop/src/apps/SettingsApp/UpdatesPanel.tsx:298-299 📄 desktop/src/apps/SettingsApp/UpdatesPanel.tsx:484-495 📄 desktop/src/apps/StoreApp/index.tsx:1167-1174 📄 tinyagentos/routes/apps.py:29-34 📄 tinyagentos/routes/apps.py:244-258

The backend optional_app_catalog returns all 9 allowlisted ids, including the five studios (coding-studio, design-studio, music-studio, app-studio, office-suite). However the frontend display registry OPTIONAL_APPS only contains 4 entries (reddit, youtube-library, github-browser, x-monitor).

In UpdatesPanel.tsx, the comment on line ~298 states: "Only show installed optional apps that have a matching registry entry for display metadata", but the actual code does not filter on registry membership — const installedOptional = optionalCatalog.filter((e) => e.installed). As a result an installed studio renders with its raw id as the display name (meta?.name ?? entry.id → e.g. "coding-studio") and the generic package icon (meta?.icon ?? "package"). The same mismatch exists in StoreApp/index.tsx's updates section (meta?.name ?? entry.id).

Suggested fix: either add the studio entries to the OPTIONAL_APPS registry so they have proper name/icon metadata, or actually filter the catalog to ids present in the registry (and make the comment truthful). Pick one consistently across both UpdatesPanel and StoreApp.

💡 Edge Case: _semver_tuple mismatched-length comparison can falsely flag updates

📄 tinyagentos/routes/apps.py:65-75 📄 tinyagentos/routes/apps.py:249-252

_semver_tuple parses versions into variable-length tuples. Comparing tuples of unequal length can produce surprising results: e.g. a recorded version "1.0" becomes (1, 0) and the current "1.0.0" becomes (1, 0, 0); since (1, 0) < (1, 0, 0) is True, update_available would falsely flip true even though the versions are semantically equal. Also, a parse failure returns (0,), which compared against a valid (0, 9, 0) yields (0, 9, 0) < (0,) → False, masking a legitimate update.

Currently all APP_VERSIONS are uniform "1.0.0" so this is latent, but it will surface once versions diverge in length. Suggested fix: normalize both tuples to a fixed length (pad with zeros) before comparing, e.g. def _semver_tuple(v): parts = ...; return tuple((parts + (0,0,0))[:3]).

🤖 Prompt for agents
Code Review: Implements per-app versioning and UI updates for system apps, but hides optional app updates when other store items exist, incorrectly displays raw studio IDs, and contains a semver comparison logic bug.

1. ⚠️ Bug: Optional app updates hidden when other store updates exist
   Files: desktop/src/apps/StoreApp/index.tsx:1159-1173, desktop/src/apps/StoreApp/index.tsx:1202-1216

   In `StoreApp/index.tsx` the updatable optional apps (`taOS Apps` section) are rendered only inside the branch `activeNav === "updates" && filtered.length === 0`. That branch is taken exclusively when there are no other updatable framework/store apps. As soon as `filtered.length > 0` (i.e. there is at least one regular store/framework update), control falls through to the `filtered.map(...)` grid at the bottom, and the optional-app updates are never displayed.
   
   This means a user who has both a framework update and an updatable optional (core) app will only see the framework update — the optional-app update silently disappears from the Updates tab. The catalog is fetched correctly, but the surface only appears in the empty state.
   
   Suggested fix: render the `updatableOptional` section above the regular updates grid regardless of `filtered.length`, rather than nesting it inside the `filtered.length === 0` empty-state branch.

2. ⚠️ Bug: Studio apps render raw ids/generic icons; comment claims filtering
   Files: desktop/src/apps/SettingsApp/UpdatesPanel.tsx:298-299, desktop/src/apps/SettingsApp/UpdatesPanel.tsx:484-495, desktop/src/apps/StoreApp/index.tsx:1167-1174, tinyagentos/routes/apps.py:29-34, tinyagentos/routes/apps.py:244-258

   The backend `optional_app_catalog` returns all 9 allowlisted ids, including the five studios (`coding-studio`, `design-studio`, `music-studio`, `app-studio`, `office-suite`). However the frontend display registry `OPTIONAL_APPS` only contains 4 entries (`reddit`, `youtube-library`, `github-browser`, `x-monitor`).
   
   In `UpdatesPanel.tsx`, the comment on line ~298 states: "Only show installed optional apps that have a matching registry entry for display metadata", but the actual code does not filter on registry membership — `const installedOptional = optionalCatalog.filter((e) => e.installed)`. As a result an installed studio renders with its raw id as the display name (`meta?.name ?? entry.id` → e.g. "coding-studio") and the generic `package` icon (`meta?.icon ?? "package"`). The same mismatch exists in `StoreApp/index.tsx`'s updates section (`meta?.name ?? entry.id`).
   
   Suggested fix: either add the studio entries to the `OPTIONAL_APPS` registry so they have proper name/icon metadata, or actually filter the catalog to ids present in the registry (and make the comment truthful). Pick one consistently across both UpdatesPanel and StoreApp.

3. 💡 Edge Case: _semver_tuple mismatched-length comparison can falsely flag updates
   Files: tinyagentos/routes/apps.py:65-75, tinyagentos/routes/apps.py:249-252

   `_semver_tuple` parses versions into variable-length tuples. Comparing tuples of unequal length can produce surprising results: e.g. a recorded version `"1.0"` becomes `(1, 0)` and the current `"1.0.0"` becomes `(1, 0, 0)`; since `(1, 0) < (1, 0, 0)` is True, `update_available` would falsely flip true even though the versions are semantically equal. Also, a parse failure returns `(0,)`, which compared against a valid `(0, 9, 0)` yields `(0, 9, 0) < (0,)` → False, masking a legitimate update.
   
   Currently all APP_VERSIONS are uniform `"1.0.0"` so this is latent, but it will surface once versions diverge in length. Suggested fix: normalize both tuples to a fixed length (pad with zeros) before comparing, e.g. `def _semver_tuple(v): parts = ...; return tuple((parts + (0,0,0))[:3])`.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Comment thread tinyagentos/routes/apps.py Outdated
"""
v = version.lstrip("v").split("-")[0].split("+")[0]
try:
return tuple(int(p) for p in v.split("."))

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: Semver tuple comparison is length-sensitive

tuple(int(p) for p in v.split(".")) makes 1.0 compare as older than 1.0.0, so an app recorded as 1.0 would incorrectly show update_available=true against the current 1.0.0. Pad or normalize components before comparing.

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

fetchLatestFrameworks().then(setLatest).catch(() => {});
fetch("/api/agents").then((r) => r.ok ? r.json() : []).then((j) => setAgentList(Array.isArray(j) ? j : (j?.agents ?? []))).catch(() => {});
fetch("/api/cluster/install-targets", { headers: { Accept: "application/json" } }).then((r) => r.ok ? r.json() : null).then((data) => { if (Array.isArray(data)) setInstallTargets(data); }).catch(() => {});
fetch("/api/apps/optional/catalog", { headers: { Accept: "application/json" } }).then((r) => r.ok ? r.json() : null).then((data) => { if (data?.apps) setOptionalCatalog(data.apps); }).catch(() => {});

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: Optional catalog is fetched only on mount

Optional app install/remove emits APP_OPTIONAL_CHANGED, but this component does not subscribe to that event. The Updates tab can therefore keep stale optionalCatalog state after installing or removing an optional app until the Store remounts.

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

@@ -1154,10 +1157,34 @@ export function StoreApp({ windowId: _windowId }: { windowId: string }) {
<span>No matches for &ldquo;{search.trim()}&rdquo;</span>
</div>
) : activeNav === "updates" && filtered.length === 0 ? (

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: Optional app updates are hidden when normal store updates exist

This branch only renders when filtered.length === 0. If there are any regular CatalogApp updates, the optional catalog section is skipped entirely, so updatable taOS apps will not surface in the Updates tab even though optionalCatalog has entries.

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: 3 Issues Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
desktop/src/apps/StoreApp/index.tsx 933 Optional catalog is fetched only on mount and can stay stale after optional app install/remove.
desktop/src/apps/StoreApp/index.tsx 1154 Optional app updates ignore the Updates tab search and are excluded from the displayed app count.

SUGGESTION

File Line Issue
desktop/src/apps/SettingsApp/UpdatesPanel.tsx 299 Comment overstates the registry guarantee; backend/frontend optional app metadata can drift and silently render raw ids/generic icons.
Resolved since previous review
File Line Issue
tinyagentos/routes/apps.py 75 Semver tuple comparison is now fixed-width, resolving the 1.0 vs 1.0.0 issue.
desktop/src/apps/StoreApp/index.tsx 1154 Optional app updates now render before the regular updates grid instead of only in the empty-state branch.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
desktop/src/apps/StoreApp/index.tsx 1045 Mobile Store updates tab likely does not surface optional app updates because StoreApp returns MobileStore before the optional catalog section, and MobileStore has no optional catalog input.
Files Reviewed (4 files)
  • desktop/src/apps/SettingsApp/UpdatesPanel.tsx - 1 issue
  • desktop/src/apps/StoreApp/index.tsx - 2 issues
  • desktop/src/registry/app-registry.ts - 0 issues
  • tinyagentos/routes/apps.py - 0 issues

Fix these issues in Kilo Cloud

Previous Review Summary (commit bba39d8)

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

Previous review (commit bba39d8)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
tinyagentos/routes/apps.py 73 Semver tuple comparison is length-sensitive, so 1.0 compares older than 1.0.0.
desktop/src/apps/StoreApp/index.tsx 933 Optional catalog is fetched only on mount and can stay stale after optional app install/remove.
desktop/src/apps/StoreApp/index.tsx 1159 Optional app updates are rendered only when normal store updates are empty, hiding them when both exist.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
desktop/src/apps/StoreApp/index.tsx 1045 Mobile Store updates tab likely does not surface optional app updates because StoreApp returns MobileStore before the optional catalog section, and MobileStore has no optional catalog input.
Files Reviewed (7 files)
  • desktop/package-lock.json - 0 issues
  • desktop/src/apps/SettingsApp/UpdatesPanel.test.tsx - 0 issues
  • desktop/src/apps/SettingsApp/UpdatesPanel.tsx - 0 issues
  • desktop/src/apps/StoreApp/index.tsx - 2 issues
  • desktop/src/apps/StoreApp/updates-optional.test.tsx - 0 issues
  • tests/test_apps_installed.py - 0 issues
  • tinyagentos/routes/apps.py - 1 issue

Fix these issues in Kilo Cloud


Reviewed by nex-n2-pro:free · 717,044 tokens

@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

🧹 Nitpick comments (4)
desktop/src/apps/SettingsApp/UpdatesPanel.test.tsx (1)

77-77: ⚡ Quick win

Simplify the fallback pattern.

The current fallback return jResp(await BASE_FETCH(url).then((r) => r.json())); extracts data from jResp and immediately re-wraps it. Since BASE_FETCH already returns the correct mock response shape, you can simplify this to:

-      return jResp(await BASE_FETCH(url).then((r) => r.json()));
+      return BASE_FETCH(url);

This pattern appears in all three optional apps section tests (lines 77, 90, 104).

Also applies to: 90-90, 104-104

🤖 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/SettingsApp/UpdatesPanel.test.tsx` at line 77, The fallback
pattern in UpdatesPanel.test.tsx at lines 77, 90, and 104 unnecessarily wraps
the result of BASE_FETCH with jResp, which extracts and re-wraps the data. Since
BASE_FETCH already returns the correct mock response shape, remove the jResp
wrapper from all three locations and return the result of BASE_FETCH directly
after awaiting and extracting the JSON response. This simplification eliminates
the redundant extraction and re-wrapping operation across all three optional
apps section tests.
desktop/src/apps/SettingsApp/UpdatesPanel.tsx (1)

487-489: ⚡ Quick win

Key optional app rows by id + source, not id alone.

Line 488 currently uses entry.id, which will collide once non-core sources are introduced for the same app id.

Proposed refactor
-                  key={entry.id}
+                  key={`${entry.id}:${entry.source}`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desktop/src/apps/SettingsApp/UpdatesPanel.tsx` around lines 487 - 489, The
OptionalAppRow component in the render function is currently using only entry.id
as the key prop, which will cause collisions when multiple sources provide the
same app id. Modify the key prop on the OptionalAppRow component to combine both
entry.id and entry.source (for example, using string concatenation or template
literals) to ensure unique keys across different sources.
desktop/src/apps/StoreApp/index.tsx (1)

845-845: ⚡ Quick win

Preserve source in state and use it in row keys.

The backend returns source, but local typing drops it and rows are keyed only by id. That makes future core/package dual entries for the same app id hard to render safely.

Proposed refactor
-  const [optionalCatalog, setOptionalCatalog] = useState<Array<{ id: string; version: string; installed: boolean; update_available: boolean }>>([]);
+  const [optionalCatalog, setOptionalCatalog] = useState<Array<{
+    id: string;
+    source: string;
+    version: string;
+    installed: boolean;
+    update_available: boolean;
+  }>>([]);
@@
-                              <div key={entry.id} className="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-shell-surface border border-shell-border">
+                              <div key={`${entry.id}:${entry.source}`} className="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-shell-surface border border-shell-border">

Also applies to: 1170-1170

🤖 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` at line 845, The optionalCatalog state
type definition in StoreApp index.tsx is missing the source field that the
backend returns, which prevents safely rendering dual entries for the same app
id with different sources. Add source as a string property to the
optionalCatalog array type definition and update any row key generation logic to
use both id and source together instead of id alone, ensuring unique
identification of each catalog entry when rendering lists or tables.
tinyagentos/routes/apps.py (1)

245-257: ⚡ Quick win

Fail fast when allowlist/version/trust maps drift.

Using .get(..., default) here hides missing config entries and silently assigns fallback version/trust. Since this endpoint is allowlist-driven, it’s safer to enforce parity and index directly.

Proposed refactor
 APP_TRUST: dict[str, str] = {
@@
 }
+
+_missing_versions = OPTIONAL_FRONTEND_APPS - set(APP_VERSIONS)
+_missing_trust = OPTIONAL_FRONTEND_APPS - set(APP_TRUST)
+if _missing_versions or _missing_trust:
+    raise RuntimeError(
+        f"Optional app catalog config drift: missing versions={sorted(_missing_versions)}, "
+        f"missing trust={sorted(_missing_trust)}"
+    )
@@
-        current_version = APP_VERSIONS.get(app_id, "1.0.0")
+        current_version = APP_VERSIONS[app_id]
@@
-            "trust": APP_TRUST.get(app_id, "first-party"),
+            "trust": APP_TRUST[app_id],
@@
-        version=APP_VERSIONS.get(app_id, "1.0.0"),
+        version=APP_VERSIONS[app_id],

Also applies to: 278-281

🤖 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 `@tinyagentos/routes/apps.py` around lines 245 - 257, Replace the defensive
`.get()` calls with default values on the APP_VERSIONS and APP_TRUST
dictionaries with direct dictionary indexing to enforce configuration parity and
fail fast when an app_id is missing from these maps. Specifically, change
`APP_VERSIONS.get(app_id, "1.0.0")` to direct indexing, and change
`APP_TRUST.get(app_id, "first-party")` to direct indexing. Apply this same
refactoring pattern to all other locations in the file where configuration maps
are accessed with `.get()` and default values, ensuring that missing app_id
entries will raise a KeyError rather than silently falling back to defaults.
🤖 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`:
- Line 933: The fetch call for the optional catalog in the StoreApp component
accepts any truthy value for data.apps without validating its shape, which
causes a crash when later code calls .filter on it. Add validation to ensure
data.apps is actually an array before calling setOptionalCatalog, by checking
both that data.apps exists and that it is an Array (using Array.isArray or
similar validation) in the promise chain where setOptionalCatalog is currently
invoked.

In `@tinyagentos/routes/apps.py`:
- Around line 65-75: The _semver_tuple function returns variable-length tuples
which causes incorrect version comparisons when tuples have different lengths
(e.g., (1, 0) compares as less than (1, 0, 0)). Modify the function to normalize
the returned tuple to a consistent width by padding shorter tuples with zeros on
the right side. This ensures semver versions like "1.0" and "1.0.0" are treated
as equal during comparisons and fixes the update_available logic for legacy
version strings.

---

Nitpick comments:
In `@desktop/src/apps/SettingsApp/UpdatesPanel.test.tsx`:
- Line 77: The fallback pattern in UpdatesPanel.test.tsx at lines 77, 90, and
104 unnecessarily wraps the result of BASE_FETCH with jResp, which extracts and
re-wraps the data. Since BASE_FETCH already returns the correct mock response
shape, remove the jResp wrapper from all three locations and return the result
of BASE_FETCH directly after awaiting and extracting the JSON response. This
simplification eliminates the redundant extraction and re-wrapping operation
across all three optional apps section tests.

In `@desktop/src/apps/SettingsApp/UpdatesPanel.tsx`:
- Around line 487-489: The OptionalAppRow component in the render function is
currently using only entry.id as the key prop, which will cause collisions when
multiple sources provide the same app id. Modify the key prop on the
OptionalAppRow component to combine both entry.id and entry.source (for example,
using string concatenation or template literals) to ensure unique keys across
different sources.

In `@desktop/src/apps/StoreApp/index.tsx`:
- Line 845: The optionalCatalog state type definition in StoreApp index.tsx is
missing the source field that the backend returns, which prevents safely
rendering dual entries for the same app id with different sources. Add source as
a string property to the optionalCatalog array type definition and update any
row key generation logic to use both id and source together instead of id alone,
ensuring unique identification of each catalog entry when rendering lists or
tables.

In `@tinyagentos/routes/apps.py`:
- Around line 245-257: Replace the defensive `.get()` calls with default values
on the APP_VERSIONS and APP_TRUST dictionaries with direct dictionary indexing
to enforce configuration parity and fail fast when an app_id is missing from
these maps. Specifically, change `APP_VERSIONS.get(app_id, "1.0.0")` to direct
indexing, and change `APP_TRUST.get(app_id, "first-party")` to direct indexing.
Apply this same refactoring pattern to all other locations in the file where
configuration maps are accessed with `.get()` and default values, ensuring that
missing app_id entries will raise a KeyError rather than silently falling back
to defaults.
🪄 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: b1d8ed99-4407-4fc1-bed6-660ecc2c5dc1

📥 Commits

Reviewing files that changed from the base of the PR and between 2444cf0 and bba39d8.

⛔ Files ignored due to path filters (1)
  • desktop/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • desktop/src/apps/SettingsApp/UpdatesPanel.test.tsx
  • desktop/src/apps/SettingsApp/UpdatesPanel.tsx
  • desktop/src/apps/StoreApp/index.tsx
  • desktop/src/apps/StoreApp/updates-optional.test.tsx
  • tests/test_apps_installed.py
  • tinyagentos/routes/apps.py

fetchLatestFrameworks().then(setLatest).catch(() => {});
fetch("/api/agents").then((r) => r.ok ? r.json() : []).then((j) => setAgentList(Array.isArray(j) ? j : (j?.agents ?? []))).catch(() => {});
fetch("/api/cluster/install-targets", { headers: { Accept: "application/json" } }).then((r) => r.ok ? r.json() : null).then((data) => { if (Array.isArray(data)) setInstallTargets(data); }).catch(() => {});
fetch("/api/apps/optional/catalog", { headers: { Accept: "application/json" } }).then((r) => r.ok ? r.json() : null).then((data) => { if (data?.apps) setOptionalCatalog(data.apps); }).catch(() => {});

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

Guard catalog payload shape before setting optionalCatalog.

Line 933 accepts any truthy data.apps. If it’s not an array, Line 1162 throws when calling .filter, breaking the Updates view.

Proposed fix
-    fetch("/api/apps/optional/catalog", { headers: { Accept: "application/json" } }).then((r) => r.ok ? r.json() : null).then((data) => { if (data?.apps) setOptionalCatalog(data.apps); }).catch(() => {});
+    fetch("/api/apps/optional/catalog", { headers: { Accept: "application/json" } })
+      .then((r) => (r.ok ? r.json() : null))
+      .then((data) => {
+        if (Array.isArray(data?.apps)) setOptionalCatalog(data.apps);
+      })
+      .catch(() => {});

Also applies to: 1162-1162

🤖 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` at line 933, The fetch call for the
optional catalog in the StoreApp component accepts any truthy value for
data.apps without validating its shape, which causes a crash when later code
calls .filter on it. Add validation to ensure data.apps is actually an array
before calling setOptionalCatalog, by checking both that data.apps exists and
that it is an Array (using Array.isArray or similar validation) in the promise
chain where setOptionalCatalog is currently invoked.

Comment thread tinyagentos/routes/apps.py Outdated
- Store: hoist the taOS Apps updates section above the grid so optional-app
  updates show even when framework updates also exist (was nested in the
  filtered.length===0 empty-state branch and hidden otherwise)
- name/icon for optional apps now come from the app registry (getApp), so the
  five studios resolve proper names/icons instead of raw ids + generic icon
- _semver_tuple pads to (major,minor,patch) so '1.0' and '1.0.0' compare equal,
  and returns (0,0,0) on parse failure so it never masks a real update
const hasPendingRestart = !!updateStatus?.pending_restart_sha;

// Installed optional apps; display name/icon come from the app registry
// (getApp covers every optional app, including the studios).

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: Backend/frontend optional app metadata can drift

The comment says getApp covers every optional app, but that guarantee lives outside this component. If the backend allowlist gains an optional app that is missing from the frontend registry, this will still render raw ids and generic icons via meta?.name ?? entry.id / meta?.icon ?? "package".

Consider keeping the backend allowlist and frontend registry synchronized through shared metadata or a startup/config validation check so this fallback is not silently relied on.

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

<p className="text-[12px] text-shell-text-tertiary mt-0.5">{filtered.length} apps</p>
</div>
</div>
{activeNav === "updates" && (() => {

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: Optional app updates ignore the Updates tab search

When activeNav === "updates" and the user types a search query, this section still renders every installed optional app with an update, even if its name/id does not match the query. The header/count below ({filtered.length} apps) also excludes these optional rows, so search results can include non-matching optional updates and the count can look inconsistent.

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

@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/index.tsx (1)

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

Updates count under-reports when optional app updates exist.

Line 1151 shows {filtered.length} apps, but optional updates are rendered from optionalCatalog separately. In Updates, this can display 0 apps while listing entries under “taOS Apps.”

Suggested fix
-                  <p className="text-[12px] text-shell-text-tertiary mt-0.5">{filtered.length} apps</p>
+                  <p className="text-[12px] text-shell-text-tertiary mt-0.5">
+                    {activeNav === "updates"
+                      ? filtered.length + optionalCatalog.filter((e) => e.installed && e.update_available).length
+                      : filtered.length} apps
+                  </p>
🤖 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 1150 - 1155, The apps count
displayed at line 1151 uses only `filtered.length`, which does not account for
updatable optional apps when viewing the Updates section. When activeNav equals
"updates", the count should include both the regular filtered updates and the
updatable optional apps from the optionalCatalog filter. Modify the count
expression to conditionally add the length of updatable optional apps
(calculated from optionalCatalog.filter((e) => e.installed &&
e.update_available)) when in the updates view, so the displayed count accurately
reflects all apps being rendered including both regular and optional updates.
🤖 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/index.tsx`:
- Around line 1150-1155: The apps count displayed at line 1151 uses only
`filtered.length`, which does not account for updatable optional apps when
viewing the Updates section. When activeNav equals "updates", the count should
include both the regular filtered updates and the updatable optional apps from
the optionalCatalog filter. Modify the count expression to conditionally add the
length of updatable optional apps (calculated from optionalCatalog.filter((e) =>
e.installed && e.update_available)) when in the updates view, so the displayed
count accurately reflects all apps being rendered including both regular and
optional updates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cb646e78-248e-4159-b77f-c03dd82b6178

📥 Commits

Reviewing files that changed from the base of the PR and between bba39d8 and 2a69c36.

📒 Files selected for processing (3)
  • desktop/src/apps/SettingsApp/UpdatesPanel.tsx
  • desktop/src/apps/StoreApp/index.tsx
  • tinyagentos/routes/apps.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • desktop/src/apps/SettingsApp/UpdatesPanel.tsx

@jaylfc jaylfc merged commit 7fc1453 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
jaylfc added a commit that referenced this pull request Jun 16, 2026
…arXNG; Automation Studio brainstorm (#968) + searx-json-default (#969)
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