Skip to content

fix(models): wire the Models app download to the real backend (was a frontend mock) (#1548)#1571

Merged
jaylfc merged 3 commits into
devfrom
fix/models-download-wiring
Jul 3, 2026
Merged

fix(models): wire the Models app download to the real backend (was a frontend mock) (#1548)#1571
jaylfc merged 3 commits into
devfrom
fix/models-download-wiring

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Root cause

The Models app's download flow in desktop/src/apps/ModelsApp.tsx never called the backend at all:

  • DownloadProgress animated a fake percentage with setInterval + Math.random() and called onDone on a timer, with no network call.
  • handleDownload just added the model id to a local downloading Set.
  • handleDownloadDone pushed a fabricated entry (filename ${model.id}-q4_k_m.gguf) into local downloaded state. Nothing was persisted, so on the next /api/models refetch (e.g. reopening the app) the entry vanished, no files were ever written, and the "complete" state was purely cosmetic.

The real backend endpoints already existed and worked (POST /api/models/download, GET /api/models/downloads/{id}, GET /api/models with variants[].size_mb/has_downloaded_variant) — the frontend simply never called them.

Fix

  • AvailableModel now carries a variantId, chosen in the /api/models mapping as the smallest variant by size_mb (falls back to the first variant; undefined if a model has no variants, e.g. the offline MOCK_AVAILABLE list).
  • handleDownload now does POST /api/models/download with { app_id, variant_id } (credentials: "include"). A missing variantId or a failed/erroring response surfaces a clear message instead of pretending to succeed.
  • DownloadProgress polls GET /api/models/downloads/{download_id} every second and drives the bar from the real percent. status: "complete" stops polling and refetches /api/models (no fabricated entry — the Downloaded Models list now reflects backend truth and survives reopening the app). status: "error" stops polling and shows the backend's error with a Retry button that re-invokes handleDownload.
  • The downloading state changed from a Set<string> to a Record<string, DownloadState> so each in-flight download tracks its own download_id, percent, status, and error.
  • isDownloaded now also honours the backend's own has_downloaded_variant verdict, since the existing union of controller/worker/cloud downloads is keyed by filename/host, not catalog model id, and would otherwise never mark a freshly-completed local download as installed.

Tests

Added desktop/src/apps/ModelsApp.download.test.tsx, mocking fetch to assert:

  • Download issues POST /api/models/download with the correct {app_id, variant_id}
  • the progress bar reflects the polled percent
  • a polled status: "error" surfaces the error text with a Retry button
  • a polled status: "complete" triggers a real /api/models refetch (asserted via a second catalog fetch flipping has_downloaded_variant), not a fabricated entry
  • a model with no variant shows a clear "no downloadable variant" error

Test plan

  • cd desktop && npm install && npm run build succeeds
  • cd desktop && npx vitest run — 278 files / 2320 tests pass
  • New ModelsApp.download.test.tsx suite passes (5/5)

Summary by CodeRabbit

  • Bug Fixes
    • Model downloads now reflect real backend progress via polling, with accurate complete and error outcomes.
    • After completion, the downloaded/installed state is refreshed from the server (no client-fabricated entries).
    • Improved messaging when no downloadable variant exists, avoiding false success states.
    • Download requests now use the selected variant for both size display and download initiation.
  • Tests
    • Added end-to-end coverage for the model download lifecycle: success, retry after error, error display, and missing-variant handling.

jaylfc added 2 commits July 3, 2026 09:53
The Models app download flow was a pure frontend mock: DownloadProgress
animated a fake percentage with setInterval + Math.random and called
onDone without ever hitting the network, handleDownload just added the
model id to a Set, and handleDownloadDone fabricated a downloaded entry
that vanished the next time /api/models was refetched.

Wire it up to the real endpoints instead:

- carry a default variantId (smallest variant by size_mb, falling back
  to the first) into AvailableModel from the /api/models catalog
- handleDownload now POSTs /api/models/download with {app_id,
  variant_id} and records the returned download_id
- DownloadProgress now polls GET /api/models/downloads/{id} every
  second and drives the bar from the real percent instead of a timer
- on status "complete" the model list is refetched from /api/models
  instead of fabricating a downloaded entry, so the Downloaded Models
  list reflects backend truth and survives reopening the app
- on status "error" (or a failed POST) the backend error is shown on
  the download card with a Retry button
- a model with no variant (e.g. the offline MOCK_AVAILABLE fallback)
  surfaces a clear "no downloadable variant" error instead of faking
  success
- isDownloaded now also honours the backend's own has_downloaded_variant
  verdict, since the union of controller/worker/cloud downloads is keyed
  by filename/host rather than by catalog model id
Adds ModelsApp.download.test.tsx pinning #1548: mocks fetch and asserts
Download issues POST /api/models/download with the correct
{app_id, variant_id}, the progress bar reflects polled percent, a
polled status "error" surfaces the error with a Retry button, and a
polled status "complete" triggers a real /api/models refetch instead of
a fabricated entry. Also covers the no-variant fallback case.
@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 Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 97240dfa-7d35-41bd-ba4a-606fbe13dff0

📥 Commits

Reviewing files that changed from the base of the PR and between ed379d1 and 4ae5046.

📒 Files selected for processing (1)
  • desktop/src/apps/ModelsApp.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • desktop/src/apps/ModelsApp.tsx

📝 Walkthrough

Walkthrough

ModelsApp now uses backend download state for model downloads instead of a client-side fake timer. It adds variant/install tracking, polls download progress and errors from the API, updates the download UI accordingly, and covers the flow with new tests.

Changes

Backend-driven download flow and tests

Layer / File(s) Summary
Data shapes and variant selection
desktop/src/apps/ModelsApp.tsx
DownloadedModel gains variantId and installed, DownloadState is introduced, and available-model derivation chooses the smallest usable variant while setting displayed size, variantId, and installed.
Download initiation and polling
desktop/src/apps/ModelsApp.tsx
handleDownload posts app_id and variant_id to /api/models/download, stores per-model DownloadState in a Record, and DownloadProgress polls /api/models/downloads/:downloadId with progress, completion, error, and retry handling.
Downloading section and per-model action UI
desktop/src/apps/ModelsApp.tsx
The Downloading section renders from Object.entries(downloading), wires progress callbacks, and updates download button state from installed, downloading, and error status.
Download flow end-to-end tests
desktop/src/apps/ModelsApp.download.test.tsx
New tests add fetch-mocking helpers and verify the download POST body, polled progress rendering, error and Retry UI, completion refetching, and the no-downloadable-variant error path.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ModelsApp
  participant DownloadProgress
  participant Backend

  User->>ModelsApp: click Download
  ModelsApp->>Backend: POST /api/models/download {app_id, variant_id}
  Backend-->>ModelsApp: download_id
  ModelsApp->>DownloadProgress: render with state.downloadId
  loop poll interval
    DownloadProgress->>Backend: GET /api/models/downloads/:downloadId
    Backend-->>DownloadProgress: percent, status
    DownloadProgress->>ModelsApp: onUpdate(state)
  end
  alt status complete
    DownloadProgress->>ModelsApp: onComplete()
    ModelsApp->>Backend: GET /api/models
    Backend-->>ModelsApp: updated catalog
  else status error
    DownloadProgress->>ModelsApp: onRetry available
  end
Loading

Possibly related PRs

  • jaylfc/taOS#1549: Also changes the models download UI to keep backend download failures visible and retryable.
🚥 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 clearly summarizes the main change: replacing the Models app download mock with real backend wiring.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/models-download-wiring

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.

@gitar-bot

gitar-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

Comment thread desktop/src/apps/ModelsApp.tsx Outdated
downloadId,
percent: state.percent,
status: "error",
error: "Lost contact with the download; retry to continue",

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: A single failed poll flips the download to error and prompts a Retry, but the backend's download is almost certainly still running. The Retry click calls handleDownload(model) which POST /api/models/downloads again — if the backend doesn't deduplicate by (app_id, variant_id), this silently starts a second concurrent download and the original is left to complete (or fail) without the UI ever knowing. Consider distinguishing poll transport errors from backend errors: on a network failure, leave the in-flight downloadId in place and just retry the next tick, only escalating to error once the backend itself returns status: "error" or a definitive non-retryable HTTP status.


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


const poll = async () => {
try {
const res = await fetch(`/api/models/downloads/${downloadId}`, {

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: The poll fetch has no AbortController and the effect has no cleanup for an in-flight request. If the component unmounts (e.g. the user navigates away, or onComplete removes the entry from downloading which unmounts <DownloadProgress>), a still-pending poll() will resolve and call onUpdate/onComplete on an unmounted component, potentially triggering React's "set state on unmounted component" warning and — worse — a stale onComplete() that triggers an extra fetchModels(). Use an AbortController ref, abort in the effect cleanup, and guard the onUpdate/onComplete calls with an aborted flag.


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

Comment thread desktop/src/apps/ModelsApp.tsx Outdated
};

const progress = Math.min(100, Math.round(pct));
timer.current = setInterval(poll, 1000);

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: setInterval does not await previous iterations, so two polls can be in flight at once if the network is slow. The slower poll's onUpdate will then overwrite the fresher state with stale percent/status. Also, timer.current = setInterval(poll, 1000) fires poll immediately at t=0 and at t=1000ms — the immediate poll can race with the parent state that just set downloadId. Prefer a self-rescheduling setTimeout chain that awaits poll() before scheduling the next tick.


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

Comment thread desktop/src/apps/ModelsApp.tsx Outdated
const isDownloading = downloading.has(model.id);
const isDownloaded =
model.installed || downloaded.some((d) => d.id === model.id);
const isDownloading = model.id in downloading;

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: model.id in downloading is true for any entry — including status: "error". The Available Models card then renders the disabled Downloading... button (line 794), hiding the failure from the most prominent surface and forcing the user to scroll up to the "Downloading" section to find Retry. Differentiate the states (e.g. isDownloading && state.status !== "error", or add an isError flag) and surface the error inline on the Available card so the user can retry without hunting.


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

@kilo-code-bot

kilo-code-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • desktop/src/apps/ModelsApp.tsx - 0 issues
  • desktop/src/apps/ModelsApp.download.test.tsx - 0 issues
Previous Review Summary (commit ed379d1)

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

Previous review (commit ed379d1)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
desktop/src/apps/ModelsApp.tsx 167 A single failed poll flips the download to error, but the backend's download is still running. Retry POSTs /api/models/download again, risking a duplicate concurrent download.
desktop/src/apps/ModelsApp.tsx 131 Poll fetch has no AbortController; an in-flight poll after unmount can call onUpdate/onComplete on a gone component and trigger an extra fetchModels().
desktop/src/apps/ModelsApp.tsx 172 setInterval does not await previous polls; concurrent polls can race and the slower one can overwrite fresher state.

SUGGESTION

File Line Issue
desktop/src/apps/ModelsApp.tsx 747 model.id in downloading is true even when status === "error", so the Available Models card shows a disabled Downloading... button instead of an inline error/Retry.
Files Reviewed (2 files)
  • desktop/src/apps/ModelsApp.tsx - 4 issues
  • desktop/src/apps/ModelsApp.download.test.tsx - 0 issues

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 47.9K · Output: 9.3K · Cached: 887.4K

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

🧹 Nitpick comments (2)
desktop/src/apps/ModelsApp.download.test.tsx (2)

45-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

mockFetch's prefix matching is order-dependent.

Routes are matched with url.startsWith(prefix) iterated via Object.entries(routes), and "/api/models" is a prefix of "/api/models/download", which is itself a prefix of "/api/models/downloads/". This only resolves correctly today because every test happens to declare the routes from most-specific to least-specific. Reordering keys (or adding a new endpoint under /api/models/...) would silently route requests to the wrong handler with no test failure hinting why.

Consider matching on exact segments or sorting by prefix length/specificity internally so correctness doesn't depend on call-site ordering:

♻️ Suggested more robust matching
-      for (const [prefix, make] of Object.entries(routes)) {
+      const sorted = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
+      for (const [prefix, make] of sorted) {
         if (url.startsWith(prefix)) return Promise.resolve(make());
       }

Also applies to: 66-71, 88-96, 112-118, 130-138

🤖 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/ModelsApp.download.test.tsx` around lines 45 - 58, mockFetch
currently resolves handlers by the insertion order of Object.entries(routes), so
prefix matches can pick the wrong endpoint when one route is a prefix of
another. Update mockFetch to choose the most specific match internally in
ModelsApp.download.test.tsx, ideally by exact path/segment matching or by
sorting prefixes by length before checking startsWith, so calls to /api/models,
/api/models/download, and /api/models/downloads/ do not depend on the call-site
order in the affected tests.

65-84: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Also assert the HTTP method for the download POST.

The test verifies the request body but not init?.method. Asserting POST would catch a regression where the app accidentally issues a GET (or another verb) with a body that gets silently dropped by some fetch implementations.

✅ Suggested addition
       const body = JSON.parse(call!.init!.body as string);
       expect(body).toEqual({ app_id: "test-model", variant_id: "q4" });
+      expect(call!.init!.method).toBe("POST");
🤖 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/ModelsApp.download.test.tsx` around lines 65 - 84, The
ModelsApp.download test currently checks the request body for
/api/models/download but does not verify the HTTP verb. Update the test around
the mocked fetch call in ModelsApp.download.test.tsx to assert that the recorded
request for /api/models/download has init.method set to POST, alongside the
existing body assertion, so regressions in the download request method are
caught.
🤖 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.

Nitpick comments:
In `@desktop/src/apps/ModelsApp.download.test.tsx`:
- Around line 45-58: mockFetch currently resolves handlers by the insertion
order of Object.entries(routes), so prefix matches can pick the wrong endpoint
when one route is a prefix of another. Update mockFetch to choose the most
specific match internally in ModelsApp.download.test.tsx, ideally by exact
path/segment matching or by sorting prefixes by length before checking
startsWith, so calls to /api/models, /api/models/download, and
/api/models/downloads/ do not depend on the call-site order in the affected
tests.
- Around line 65-84: The ModelsApp.download test currently checks the request
body for /api/models/download but does not verify the HTTP verb. Update the test
around the mocked fetch call in ModelsApp.download.test.tsx to assert that the
recorded request for /api/models/download has init.method set to POST, alongside
the existing body assertion, so regressions in the download request method are
caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 991fd0f5-832c-4f8c-92dc-fbbd44eebdd4

📥 Commits

Reviewing files that changed from the base of the PR and between 89bc7ab and ed379d1.

📒 Files selected for processing (2)
  • desktop/src/apps/ModelsApp.download.test.tsx
  • desktop/src/apps/ModelsApp.tsx

… races, error card)

Fold review findings on the download wiring:
- a single failed poll no longer flips a still-running backend download to
  error and prompts a duplicate re-download; only a real backend status of
  error, or 3 consecutive poll misses, ends the download
- the poll fetch now uses an AbortController and a cancelled flag so an
  in-flight poll after unmount cannot call setState / refetch on a gone view
- replaced setInterval with a self-scheduling awaited poll so a slow response
  can never overlap or clobber a fresher one
- the Available Models card now shows an actionable Retry (not a stuck
  disabled Downloading...) when a download has errored

Docs-Reviewed: frontend-only change plus a new test file inside the existing Models app; no desktop app or API route added or removed, so README needs no change.
@jaylfc jaylfc merged commit d9060ec into dev Jul 3, 2026
9 checks passed
@jaylfc jaylfc deleted the fix/models-download-wiring branch July 3, 2026 09:41
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jul 3, 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