Skip to content

Feat/core migration image generation - #303

Merged
Vect0rM merged 290 commits into
feat/atomic-chat-core-migrationfrom
feat/core-migration-image-generation
Sep 22, 2026
Merged

Vect0rM merged 290 commits into
feat/atomic-chat-core-migrationfrom
feat/core-migration-image-generation

Conversation

@Vect0rM

@Vect0rM Vect0rM commented Sep 18, 2026

Copy link
Copy Markdown
Member

Describe Your Changes

Fixes Issues

  • Closes #
  • Closes #

Self Checklist

  • Added relevant comments, esp in complex areas
  • Updated docs (for bug fixes / features)
  • Created issues for follow-up changes or refactoring needed

danyurkin and others added 30 commits September 16, 2026 18:20
… engine update checks)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…that run here

With a text-only model selected, "Add images" opened a small popover that
named one hard-coded model in untranslated English and never said what was
wrong. Replace it with a dialog that says the model in use can't see images
and lists the vision models this machine can run, each with Download (or
Use when already on disk), plus a row to the Hub for the rest of Hugging
Face.

- `useVisionDownloads`: the manifest's vision entries for the tier plus
  staff picks curated as `vision`, GGUF only, kept only when the card ships
  a projector, memory-gated (`wont_load` dropped), best fit first, deduped,
  capped at five. Modelled on `useRecommendedDownloads`.
- `VisionModelDialog`: `RouteRow`s like the reply-model gate; the import
  listener stays mounted so the composer still switches when a download the
  user closed the dialog on lands.
- `useStaffPicks` takes an optional predicate so the dialog resolves only
  vision picks instead of the whole curated list.
- Remove `PromptVisionModel` and the `JAN_V2_VL_*` constants; no model name
  lives in code any more.
- Locale keys under `chat:visionGate`; ADR
  2026-09-16-explain-why-images-need-a-vision-model.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ting tools

The composer's Plugins menu listed every connector as "N tools", which says
nothing to someone deciding whether to switch it on, and an off connector had
no second line at all. Every catalog entry now carries a `taglineKey`
("Web search", "Google search", "Issues & projects"); the menu shows it under
the name for any server the catalog recognizes, on or off. The count / cost
line and the heavy warning move to the row's tooltip for those rows. Servers
the catalog does not know keep the count line as before, and "Off for this
chat" still wins while a connector is muted.

Adds English `mcp-connectors:taglines.*` for all 23 catalog entries and a
catalog test that every entry has a tagline that exists in the locale.

ADR: docs/decisions/2026-09-16-name-connectors-by-what-they-do.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-generation-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e-generation-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ration-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ation-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…age-generation-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eration-fixes

Re-applied the provider-header ellipsis hunk onto the re-indented list body.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eration-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…generation-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ration-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…arallel UI fixes

Scaffolding for the onboarding polish batch: the fit badge needs a place
beside a row's title, and every action button in these lists should share
one class so the parallel fixes converge on one column. No behaviour change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…asked

The "Or get a model another way" rows on the onboarding screen — shared
with the composer's "Pick a model to start chatting" dialog — carried copy
Danny flagged on the 2.0.39 debug build: the Hugging Face row was titled
"Any model from Hugging Face" with a hint about GGUF and MLX builds, and
the cloud row's hint ("Use your OpenRouter, Anthropic, Gemini or OpenAI
key", 52 characters) overran the one clamped line a row gives it — about
260 px beside the mark and the width-reserving button under the 520 px
onboarding column, some 40 characters of 12 px Inter — and ended in an
ellipsis.

- huggingFaceTitle: "Any model from Hugging Face" → "Hugging Face models"
- huggingFaceHint / huggingFaceHintGguf: "Browse GGUF and MLX builds in
  Models" / "Browse GGUF builds in Models" → "Add any model" (both)
- huggingFaceTrigger: "Browse models on Hugging Face" → "Browse Hugging
  Face models"
- providerHint: "Use your OpenRouter, Anthropic, Gemini or OpenAI key" →
  "OpenRouter, Anthropic, Gemini, OpenAI"

English only; other locales fall back. The two component tests gain an
English-mode switch on their i18n mock and assert the rows in the words
the user reads, including the buttons' accessible names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…raps

Danny's 2.0.39 test drive: the "downloaded GB" readout in the bottom-right
download panel wrapped onto a second line and the card shifted. The row's
readout was two flex spans and only the right one had `nowrap`; the row has
320 px, the worst realistic content needs 299.5 px at the default font size
(measured with the bundled Inter) and 336 / 372 px at the Large / Extra
Large settings, because `text-xs` scales with `--font-size-base` while the
panel's `22rem` does not. Past the limit the left span broke at its spaces,
the row grew, and the bottom-anchored card moved up.

The row now renders one `truncate tabular-nums` line — percent · downloaded
of total · time left — built by `formatDownloadReadout` in
`lib/downloadFormat.ts`, the same string the reply gate quotes. The parts
are ordered by importance, so a line that still cannot fit loses the tail of
the estimate to an ellipsis and never wraps. The transfer speed is no longer
shown in the panel; the estimate already folds it in. The panel keeps its
fixed `min(22rem, calc(100vw - 2rem))` width, which the one-line readout
fits at every font setting (225 px at Medium, 281 px at Extra Large).

Tests: the readout line, its classes, the paused and no-rate variants, the
name truncation, the controls slot, the panel's fixed width class with one
and three rows, and the collapsed badge. ADR recorded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A red fit mark meant the file would download and then refuse to load,
but the row's Download button started the transfer regardless. Now a
red row asks first: "This model won't fit in memory", with the row's
name and the mark's own sentence in the machine's figures, Cancel as
the default answer and "Download anyway" running the click's original
body unchanged, so the memory question comes before the disk-space one.
Yellow and green rows, and rows with no verdict, start at once as
before.

One hook (useConfirmWontFitDownload) and one dialog
(ConfirmWontFitDownload), wired at both download lists: the onboarding
picker and the reply-model gate, which judges the file its row would
fetch through the same describeRecommendationFit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ion-fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Danny's layout feedback on the first-run screen (64 GB M5 Max): the fit
marks drifted out of line across rows, the Download buttons were padded to
the width of "Downloading…", the size hung off the model name, the progress
readout shoved the button aside, and the route rows' marks were half the
size of the model logos.

- The fit mark is a badge with a word — Fits / Tight / Won't fit — in the
  source-badge style, on the name line; the name truncates, the badge never
  wraps under it. `ModelFitIndicator` keeps its props, accessible name and
  tooltip and translates the word itself (`setup:recommend.fitBadge*`).
- The size rides inside the button ("Download 2.5 GB"; plain "Download"
  when unknown; "Downloaded" as before).
- One width for every action button on the screen and in every `RouteRow`
  list: `ROUTE_ROW_ACTION_CLASS = 'min-w-[9.25rem] shrink-0 rounded-full
  px-3'`, sized for "Download 19.7 GB" measured in Inter Medium 14 px. The
  `RowActionLabel` width-reserve hack and `rowActionLabels` are retired.
- While a download runs the slot shows the same-width "Downloading… ×" pill
  and the readout takes the hint line under the name (`aria-live` kept); the
  offer keeps that line, empty, so its button never moves.
- `RouteRow`'s icon slot draws glyphs at 20 px and lets brand images fill
  the 32 px circle; the content column is 640 px instead of 520 px.

Tests: SetupScreen.test.tsx +5 (badge word and placement, size in the
button, one button per row with the readout under the name, one button
column without ghost labels, route mark sizing), 50 passing; ReplyModelGate,
PromptOnboardingModel and SetupScreenHelpers unchanged and green.

ADR: docs/decisions/2026-09-17-one-button-column-and-a-fit-badge-on-the-onboarding-rows.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n the ADR index

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…izes included, and seat the folder route among the routes

The blocked-send widget showed three rows from the manifest's tail under no
label, with no fit mark, a bare "Download", a three-line subtitle and a
fill-less "Add a folder with models" link under the routes. Onboarding had
moved on to the offer plus the Hub's picks listed by fit and marked row by
row; the widget now shows that list, row for row, through a new
`useRecommendedListDownloads`: the same "Recommended models" label, the
same `ModelFitIndicator` in `RouteRow`'s meta slot, the download size on
the button ("Download 4.2 GB"), and a one-line subtitle. The folder route
is a `RouteRow` among the other routes ("Models on this computer / Pick a
folder and they're added / Add") and reads "Looking…" while it scans.

The pure pickers and the fit copy moved verbatim from SetupScreen.tsx to
SetupScreenHelpers.ts and are re-exported from the screen, so the hook can
share them without importing the screen and the registry stores it pulls in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On the Models page the feed under the picks began with a muted text-xs
caption reading "More from Hugging Face · trending now", which got lost
between rows; and the picks had had no heading of their own since the
filter bar was rebuilt, so the page did not read as two sections.

- The first pick row now carries a "Recommended" heading and the first
  feed row "More from Hugging Face", both an h2 in one base-size semibold
  style, painted above the row the way the caption was.
- The heading no longer names the sort: that is what the sort dropdown is
  for. `feedTitle` drops its `{{sort}}` interpolation and the orphaned
  `feedSort` labels go with it (English only; other locales fall back).
- The route test asserts both headings by role and level, their shared
  class, the English copy, and that search results stay heading-free.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…arning to two lines

The composer's approval menu pinned each row's icon and checkmark to the
title line (`items-start` + `mt-0.5`), which put them at the top of the
70-86 px title + description block: on the three-line Full access row
they read as "flown up", and the fixed 2 px nudge sat above the title at
the Large and Extra Large font settings. The rows now use the menu
item's own `items-center`, so both sit on the middle of the block.

The "Enable Full access?" dialog wrapped its body to four lines at the
default width. It is now `sm:max-w-xl` (576 px, 526 px of text per line)
and the copy is 132 characters instead of 179, keeping every fact -
files modified or deleted, commands run, the internet used, only for a
task you trust - in two lines at the Medium and Large font settings
(three at Extra Large). Title and button labels are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nd goes

Switching providers on the Cloud page moved the whole content block a few
pixels sideways. A provider with a long model list scrolls, one with a
short list does not, and with classic (non-overlay) scrollbars — macOS with
a mouse connected, Windows, Linux — the bar's width came out of the scroll
container's content box, so the centred column was re-centred in a narrower
or wider box on every switch across that line.

Reserve the gutter on the route's scroll container (`scrollbar-gutter:
stable`, the rule SetupScreen already applies) so the box is the same width
whether or not the bar is showing. The cards themselves were already
identical across providers; a test now pins that too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Vect0rM and others added 17 commits September 21, 2026 16:48
…generation

Brings in the base's desktop e2e runner slices, the factory reset that
removes the core's folder, the stream end on the channel, the API
listener recovery, the plugin-client contracts and the other fixes of
2026-09-18..21.

Conflicts:
- factory reset (app/constants.rs): both sides started removing
  `atomic-core`; it is listed once, JAN_DATA_FILES is the union of both
  sides and the base's BACKEND_PRESERVING_PROVIDERS is kept. The reset
  still stops the core before deleting anything.
- system/commands.rs: the release line's zcode_tests and the base's
  factory_reset_tests both kept.
- ChatInput.tsx: the release line's menu title plus the base's
  "MCP tools auto-approved" label.
- extensions/yarn.lock: regenerated from the merged core package
  (both sides changed core/, so neither side's hash was right).
- docs/decisions/INDEX.md: entries from both sides; section counts and
  the total recounted (345 records, every ADR file listed once).

Semantic fix: the release line dropped the `isAnyChatBusy` import with
the import-time auto-switch, while the base added a new use of it in
handleCoreSessionDied; the import is restored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test-quality guard flagged restore-server-model.test.ts (from the
base): the first test only checked that ensureModelForServer was called.
It now asserts which model is handed over, the served one over the
default, as the neighbouring fallback test already does. Checked against
a copy of restoreServerModelAfterRecovery that prefers the default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The family id `krea-2-turbo` resolves to the `bfl` mark, as FLUX.1 Krea
does, but no name rule matched "Krea 2 Turbo", so a card drawn from the
family name showed no mark. A `krea 2` rule now gives it the same one.

The catalog test asserted only that some name rule matched; it now
requires the name's mark to equal the id's for every family, the
mismatch this was. Checked against model-logo.ts without the new rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… GiB tiers

The client knows vram_128, vram_128_plus, unified_128 and
unified_128_plus, and the published manifest lists them since
atomic-chat-conf 34ecae48, but the fixture was still the 2026-09-11
copy, so tests/registry-contracts.test.mjs failed on the missing tiers.
The fixture is now a verbatim copy of models/recommended.json at that
commit; sources.json records where it comes from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…al engine errors to it

atomic-chat-core now reports its own failures (crashes, failed loads from any
caller, engine crashes, compute errors) to its own Sentry project, only under
the app's productAnalytic consent.

- The core is launched with `daemon --telemetry on|off` from the Rust gate.
- set_telemetry_consent/context/user keep what they learn
  (telemetry/core_state.rs) and PUT it to /atomic/v1/telemetry on every
  change and on every core snapshot; the push never starts a core
  (AtomicCoreClient::call_attached).
- switchModel.ts captures model-load failures for cloud providers only;
  local engine failures are the core's, with the engine's context.
- Wire fixtures tests/fixtures/core-contracts/telemetry (dump_fixtures in
  atomic_core/telemetry.rs, comparator telemetry-wire), a live test on a
  real core, and the ADR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…me from its picker

The first-run screen stayed up until a model was on disk too, so with the
engine installed and a checkpoint downloading the page still showed the
setup card. The engine is now the only gate:

- no engine: the setup card, as before (centered, or beside existing images)
- engine installed: the studio, with or without a model; the empty canvas
  offers "Download model", which opens the form's model picker
- the wizard tour is intro -> engine -> Done; the model list stays as a
  standalone view for Media settings, which has no picker of its own
- the "download" error action opens the picker instead of the wizard

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ad never fetches

listInstalledArtifacts counted every plan entry that is not on disk as
missing, including Qwen-Image-2.1's optional llm_vision projector.
downloadArtifact fetches required entries only, so a freshly downloaded
Qwen-Image-2.1 stayed `complete: false` forever: the row said Downloaded
(it reads the workflow plan) while the wizard's Done stayed disabled and
the page kept treating the model as absent.

Completeness now follows the same required set the download uses. Edit and
Reference still ask for the projector through their own workflow plan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The family id `krea-2-turbo` resolves to the `bfl` mark, as FLUX.1 Krea
does, but no name rule matched "Krea 2 Turbo", so a card drawn from the
family name showed no mark. A `krea 2` rule now gives it the same one.

The catalog test asserted only that some name rule matched; it now
requires the name's mark to equal the id's for every family, the
mismatch this was. Checked against model-logo.ts without the new rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 8647144)
… GiB tiers

The client knows vram_128, vram_128_plus, unified_128 and
unified_128_plus, and the published manifest lists them since
atomic-chat-conf 34ecae48, but the fixture was still the 2026-09-11
copy, so tests/registry-contracts.test.mjs failed on the missing tiers.
The fixture is now a verbatim copy of models/recommended.json at that
commit; sources.json records where it comes from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 254c29a)
* feat(downloads): enhance download stage reporting and proxy connection testing

- Introduced `DownloadStage` to provide feedback during download retries, improving user experience by indicating connection attempts and retries.
- Updated download management to handle stage updates without affecting progress reporting.
- Added `test_proxy_connection` command to validate proxy configurations, ensuring users receive clear feedback on connection issues.
- Enhanced error handling in the download process to differentiate between network failures and proxy-related issues, improving overall reliability and user guidance.
- Updated relevant tests to cover new functionality and ensure robustness in download handling.

* feat(engine): implement model load cancellation and progress reporting

- Added `ModelLoadStage` type to track loading stages, enhancing user feedback during model loading.
- Implemented `cancelLoad` method in `AIEngine` to allow users to cancel ongoing model loads, returning a promise that resolves to indicate success or failure.
- Updated `load` method to accept `ModelLoadOptions` for progress reporting.
- Enhanced tests to cover new cancellation functionality and ensure correct behavior during model loading and cancellation scenarios.
- Updated documentation to reflect changes in model loading and cancellation processes.

* feat(images): integrate ImageIcon and enhance UI components

- Replaced SparklesIcon with ImageIcon in NavMain and ImageSetupDialog for better visual representation.
- Updated ImageEmptyState to use IconPhoto, improving the empty state display.
- Refined ImageArtifactDownloadButton and ImageJobProgress for better user experience and layout consistency.
- Enhanced ImageModelSelector and ImagePromptForm with improved styling and functionality.
- Introduced ImageField and ImageFieldHint components for better form handling and user guidance.
- General UI improvements across image-related components for a more cohesive design.

* release: v2.0.38

* fix(agent): classify a cut-off tool call by its cause and give the repair the step's budget

An unclosed tool-call array surfaced as `grammar: ... Repair failed: ...
tool-call JSON value is incomplete`. Under GBNF the shape cannot be wrong;
the server had stopped generating, but the client never read why, and the
repair was pinned to 1,024 tokens regardless of the batch it had to re-emit.

- Replace `CompletionResult::truncated` (prompt truncation on llama-server,
  `finish_reason == "length"` on chat transports, no readers) with
  `stop_reason: StopReason` and `prompt_truncated`.
- Keep the single repair round trip; when its output does not parse and the
  server stopped on the limit, fail as `context` (prompt truncated too) or
  as the new `OutputTruncated` / `budget` error that names the budget and
  what the user can change.
- Give the repair the step's budget less the repair block it appends; keep
  the 1,024 cap only when there is nothing to re-emit (timed-out step).
- Echo the batch after the reasoning prelude, return the repair completion
  so turn usage counts it and the next repair echoes the latest output.

ADR: docs/decisions/2026-09-15-fail-truncated-tool-calls-as-budget-and-size-the-repair.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 4235068e4d5f5baa51dd64bd98afdb4df9d0af85)

* fix(agent): restore loaded skills from disk instead of the session copy

`LoadedSkills::restore` kept the session-persisted body of a loaded skill
whenever its manifest `version` still matched, so an edit to `SKILL.md`
without a version bump never reached a thread that had already loaded the
skill, although the docs promise edits take effect on the next message.

Treat the persisted entry as a record of what was loaded: on restore,
rebuild body and version from the registry the turn just loaded (one
`loaded_body` helper shared with `skill.view`), keep order and `loaded_at`,
drop entries no longer enabled. A version bump refreshes the entry instead
of evicting it.

ADR: docs/decisions/2026-09-15-restore-loaded-skills-from-disk-not-from-the-session.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit b2ed84547982b3bcc087613d96442bddc18c91ae)

* fix(projects): say what a file upload is waiting on and surface a failed listing

The Files section of a project showed only a spinner on the Upload button
while a file was ingested. The first upload in an install also downloads
and starts the embedding model inside the same promise, with no progress
and no cancel, so it looked like the upload had hung. A failed
`listAttachmentsForProject` was swallowed and rendered the empty state.

- While an upload runs, render a status line: the embedding-model download
  with its percentage (from the existing download store, keyed by
  `EMBEDDING_MODEL_ID`), otherwise an "indexing" note that explains the
  first-upload download. The live region stays mounted so screen readers
  announce the change.
- When the listing fails, render the error with a Retry button instead of
  the empty state.

ADR: docs/decisions/2026-09-15-say-what-a-project-upload-is-waiting-on.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 3c33e0ff207532fd201a5f1ebd7cf81cbd555ee2)

* docs(decisions): align the upload feedback ADR with the #289 re-entry fix

* feat(chat): rewrite the reply gate around shared model routes and the tier's list

The composer's "what do I reply with?" widget described the state ("Nothing
to reply with yet"), offered one model from the built-in ladder — not the
manifest onboarding reads — and ended in two wide buttons that read as an
afterthought.

- Title and body now say what to do: pick a model, the message is saved.
- The recommendation is the same list onboarding leads with
  (`useRecommendedDownloads` over `useResolvedRecommendedModels`): the tier's
  best fit with a filled button, the other options as secondary rows, GGUF
  only, no sizes on the choice.
- The routes are rows shared with onboarding (`RouteRow`): "Any model from
  Hugging Face" leaves for the Hub (new `hub` outcome), ChatGPT subscription
  wears its mark, the API-key route is called that.
- Onboarding gets the same Hugging Face row; its "other ways" section always
  shows now. "Cloud provider" is "API key" on both surfaces.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(hub): list the rest of Hugging Face under the picks, a page at a time

The Models page opened on the curated picks and stopped there; Hugging Face
was reachable only through a search box.

- `listHuggingFaceFeed` asks `/api/models` for one format (`gguf` / `mlx`)
  in one of Hugging Face's own orders (trending by default, or what the
  sort dropdown names), fifty at a time, following the `Link` cursor. In
  the app it goes through the Tauri HTTP plugin so that header is readable.
- `useHuggingFaceFeed` keeps the pages across visits, asks for the next one
  a few rows before the end of the list, and fetches the card — sizes and
  quants — only for the rows on screen, two at a time, never twice.
- The hub appends the feed under the picks as its own section: a repo the
  catalog knows is shown from the catalog, one already scrolled to shows
  its fetched card, the rest stay lightweight; a size the row does not know
  yet cannot fail the fit filter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): tighten the reply gate after review, and record both decisions

- RouteRow sizes only its direct image child, so a ModelLogo in the slot
  keeps its mark; the title is not a heading.
- The recommended list waits for the lead instead of promoting the next row,
  drops other options that would not load here, and stops waiting for a
  card that never resolves.
- The Hugging Face hint names MLX only on macOS; stale keys dropped from the
  ru/ja chat locales.
- ADRs for the gate/onboarding rows and the Hub's Hugging Face feed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(hub): settle the feed review findings

- One in-flight ticket per page request, so a sort switched mid-flight
  cannot clear the new request's guard and ask for the same cursor twice.
- "Size known" is what the row can show (`modelDownloadSizeText`), so MLX
  cards go through the fit filter and catalog-known rows are not re-fetched.
- A selected feed row gets its card even when scrolled out of view.
- Plain fetch first: Hugging Face exposes the Link header cross-origin; the
  Tauri HTTP plugin stays as the fallback. ADR corrected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(vector-db): embed project file chunks once, not twice

ingestFileForProject embedded every chunk twice: once to learn the
embedding dimension before creating the collection, and again after the
duplicate check. Keep the first pass's vectors and reuse them for the
insert, halving the embedding work for a project file upload. The thread
path already embedded once.

Add the extension's vitest harness (vitest.config.ts + test scripts,
mirroring llamacpp-upstream-extension) with a regression test that
asserts the embedder sees each chunk once and every chunk is stored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(onboarding): offer the manifest's recommendation on the reminder card

The bottom-right "get started with X" card still read the bundled ladder
through useRecommendedLocalModel, so a manifest override on the user's tier
made it recommend a different model than the composer's widget and the first
screen. It now shows the first item of useRecommendedDownloads(1): the
manifest's best fit for this machine, stepped down until it loads, with the
pinned quant and projector the widget would download.

While the lead is unresolved the card renders nothing; after the widget's 8 s
budget it gives up for the session and stays hidden even if the lead arrives
later, without clearing the reminder, so the next launch gets a fresh try.

useRecommendedLocalModel had no other consumer and is deleted.
ONBOARDING_REMINDER_MODELS stays: the reminder store derives its "already on
disk" tokens from it. The card's tests now mock useResolvedRecommendedModels
and useModelSources the way ReplyModelGate's do; the per-tier ladder cases
are covered by useResolvedRecommendedModels.test.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(llamacpp-upstream): bound the model load's wait on backend configuration

`load()` awaited `configureBackendsPromise` with no bound when the backend
tag was an unresolved `latest/<backend>` sentinel (ATO-124) or a concrete tag
with no exe on disk (ATO-233). The promise covers the catalog fetch through
the Tauri HTTP layer, where a stalled connection can leave it pending forever,
so every model load hung with the spinner up — including the embedding load
behind a project file ingest.

Both waits now go through `waitForBackendConfiguration`, a `withTimeout` of
20 s (the bound `checkForEngineUpdate` already uses). Past it the load goes
ahead with what is on disk: `performLoad` resolves a leftover sentinel itself
and `ensureBackendReady` prefers an installed build of the same variant before
any download, so neither race the waits guarded against can return. Decision
and residual risks recorded in
docs/decisions/2026-09-16-bound-the-model-load-wait-on-backend-configuration.md.

Tests: a never-settling configuration no longer holds the load past the
bound (sentinel and not-installed cases); a configuration that finishes in
time is still waited for and its swapped backend is the one launched; an
installed backend never waits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(agent): reserve the thinking budget on top of the tool-call budget

The thinking block and the tool-call array share one completion cap
(`n_predict` on llama.cpp, `max_tokens` on chat transports), but the cap
was the 8,192-token tool-call budget alone. At `xhigh` (8,192 thinking
tokens) a model that used its budget had nothing left for the array, and
at `max` (uncapped) any long thought did the same; the step then failed
with the `budget` category.

Compute the step budget once per turn as the tool-call budget plus the
thinking reserve — the level's budget, or the top finite tier (8,192) for
the uncapped level, whose sampler budget stays -1 — and use it for both
the request and the context reservation, so prompt plus completion still
fit `n_ctx`. Chat transports pick the same total up through
`request.max_tokens`.

Tests assert the `n_predict` and `reasoning_budget_tokens` the scripted
llama-server receives at every level, the `max_tokens` the scripted mlx
endpoint receives, and that the conversation cap shrinks by the reserve.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore: true up the ADR index total and run the vector-db extension tests in the gate

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(images): enhance image generation workflows and input handling

- Introduced `ResolvedInputs` struct to encapsulate image inputs for various workflows.
- Updated `build_img_gen_request` to utilize `ResolvedInputs`, improving clarity and maintainability.
- Implemented `resolve_inputs` function to convert request images into base64 format, ensuring compatibility with the server.
- Enhanced `ImageGenerateRequest` to include structured image sources, replacing raw paths with `ImageSource` enum.
- Updated session and state management to support new image workflows, including reference-guided generation.
- Improved UI components to reflect new workflow capabilities and ensure a cohesive user experience.
- Added tests to validate new functionality and ensure robustness in image processing workflows.

* release: v2.0.39

* feat(images): implement engine update checks and UI enhancements

- Added functionality to check for engine updates based on the manifest, improving user experience by notifying users of available updates.
- Enhanced the MediaSettingsPanel to display update options, including a button to force check for updates and a notification for up-to-date status.
- Updated the image generation store to manage engine update state, including checking and updating the engine based on the latest manifest.
- Improved localization files to support new update-related messages across multiple languages.
- Added tests to validate the new update functionality and ensure correct behavior in various scenarios.

* fix(cloud): open the Cloud page on OpenRouter when nothing is connected yet

With no provider connected and none named in the URL, the page opened on
an empty picker and a "Pick a connection above" line. It now lands on
OpenRouter, whose one key unlocks most of the catalog, and failing that
on the first cloud provider, so the page never opens blank.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ui): give secondary buttons a visible fill in the light theme and name providers in the cloud row

In the light theme `bg-secondary` sits a shade off white, so secondary
buttons on a white card had no visible fill: the onboarding's Download,
Connect and Add pills blended into the card. The variant now rests on
neutral-200 and hovers to neutral-300 in light; dark keeps its tokens.

The onboarding's cloud row is titled "Cloud provider" again and its hint
names the providers people know: OpenRouter, Anthropic, Gemini, OpenAI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): confirm Full access before enabling it, and reword the approval rows

Picking Full access in the composer's approval menu used to switch the mode
at once. It now opens a dialog ("Enable Full access?") that says what the
mode lets tool calls do; only "I understand" calls onChange('skip'), while
Cancel, Escape and the close button leave the mode unchanged. The dialog is
per choice, not remembered. "Ask for approval" never asks.

The two rows take the wording Danny picked: "Always ask before tool calls
edit files or use the internet" and "Unrestricted: no approval prompts for
any tool call, including the internet and any file on your computer". No
sandbox is mentioned (Atomic Chat has none). Full access keeps no colour of
its own; the accept button is the normal primary style.

ADR: docs/decisions/2026-09-16-confirm-full-access-before-enabling-it.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(model-picker): offer downloads in the composer's model list

With nothing downloaded the selector's list was blank, and a search with
no local match ended in "No models found" with nothing to download from.

- Empty list: show the device's recommended downloads (the same rows the
  blocked-send widget offers, via useRecommendedDownloads) with a plain
  "Download" button, downloading hint and 8 s give-up.
- Search: list Hugging Face's GGUF repos under the local matches, one
  request per query settled for 300 ms, stale answers dropped. A row
  resolves its file on click with the Hub's own pickDownloadQuant rule
  and starts pullModelWithMetadata; the row then reports the download.
- searchHuggingFaceCandidates rethrows on a failed request so the list
  can say Hugging Face was unreachable instead of "nothing found"; the
  Hub already caught the rejection.
- "No models found" stays only when neither side has a match; the
  bottom "Download a model" row still leads into the Hub.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(updater): unfold the release notes inside the app-update banner

"Show release notes" on the bottom-right app-update banner used to leave the
app for the GitHub release page. It now expands the banner in place: the
four-bullet highlights inset gives way to a 10 rem scroll box with the full
release body rendered as markdown, a small right-aligned "Open release" link
under it takes over the browser hand-off, and the toggle reads "Hide release
notes" until the user folds it back. The header and the Remind me later /
Update row do not move.

`<UpdateBanner />` gains three optional presentational props (`expanded`,
`expandedContent`, `expandedAction`); the engine banner passes none and is
unchanged. The body is rendered with the already-bundled `react-markdown` +
`remark-gfm` rather than the chat's `<RenderMarkdown />`, which would pull
streamdown, mermaid and KaTeX into a toast. Links inside the notes go through
the opener like "Open release". An empty release body keeps the old
behaviour: the link opens the browser directly.

Locale: add `updater:openRelease`, and fix the casing of `hideReleaseNotes`
to match "Show release notes".

ADR: docs/decisions/2026-09-16-unfold-release-notes-inside-the-update-banner.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(i18n): stop calling the data folder the Jan data folder in download errors

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(assistant): migrate Jan-branded default prompts to Atomic Chat

Installs that ran upstream Jan's own migration v2 still carry Jan's default
system prompt ("You are Jan, a helpful AI assistant who assists users with
their requests. Jan is trained by Menlo Research (https://www.menlo.ai)...")
in assistants/<id>/assistant.json, so the model introduces itself as Jan.
The rebranded v2 never re-runs for them (.migration_version is already 2),
and its period-terminated prefix "You are Jan, a helpful AI assistant."
would not match that text anyway.

Add migration v3 (CURRENT_MIGRATION_VERSION = 3): for every assistant whose
instructions start with "You are Jan" or mention Menlo Research / menlo.ai,
replace the instructions with the extension's default Atomic Chat prompt,
rename a "Jan" assistant to "Atomic Chat" and fix Jan's default description.
Sampling parameters and every other field are preserved; user-authored
prompts that merely mention Jan are left alone. The v2 log line no longer
says "Menlo".

Add the extension's vitest harness (vitest.config.ts, test scripts, vitest
devDependency, lockfile entry) mirroring vector-db-extension, wire it into
`make test-extensions`, and cover the Menlo prompt, the v1 prefix, a user
prompt, parameter survival and idempotency by asserting the written
assistant.json.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(onboarding): stay on the Welcome screen until the download lands

A Download click on a recommended row armed a 3 s timer and then opened
the chat with the model whatever the download was doing; a cancel from
the download panel after that left an empty chat with no way back to the
list. The row also stacked "Starting download…" and "Download started —
opening chat…" under its greyed button, so it jumped taller.

Drop the timed handoff. The screen stays while the download runs; the
chat opens with the model only once it is in the library, through the
existing import listener (reported as the download exit it is) with an
effect on the library as the backstop, both behind hasNavigatedRef. The
row's button slot holds one pill on one line — "Downloading…" with an ×
that cancels the way the Hub's pill does — and the progress beside it, so
the row keeps its height. Cancel restores the Download button. Remove
the two orphaned keys from en, ru, ja and ko.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(setup): mark each recommended row with its fit and list the picks by it

Every row on the first-run picker now wears a small circled mark next to its
name/size line: green when the model fits this machine's memory comfortably,
yellow when it will run but tight or spilling into system RAM, red when it
will not load. The mark is a focusable button whose accessible name is a
short level label plus the same `setup:recommend.why*` sentence the offer's
badge already carries, and that sentence is its tooltip. A row whose size or
machine is unknown wears nothing.

The Hub picks are listed green, then yellow, then red, then unknown, with the
publisher interleave kept inside each group and the offer still first. The
verdict is `judgeMemoryFit` on the size the row shows, so mark and order
agree. `fitLevel` and `orderRowsByFit` are pure helpers in
`SetupScreenHelpers.ts`; `ModelFitIndicator` is the mark. On a CPU-only
machine the badge keeps explaining the offer by the CPU while the mark reads
the memory fit through a `memoryOnly` option on `describeRecommendationFit`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(downloads): check disk space before a download starts

The Rust downloader already refuses a transfer the volume cannot hold, but
the refusal reached the web as a download error: every entry point had
flipped its row to "Downloading" before "Not enough disk space" arrived,
which from onboarding read as a broken download rather than a model too
big for the drive.

- Add `get_download_free_space` (Tauri command, desktop and mobile handler
  lists) returning `{ available, headroom }` for the data folder's volume,
  built on `available_space_for` / `FREE_SPACE_HEADROOM`.
- In `pullModelWithMetadata`, before anything is recorded or started, ask
  it with the size the caller knows (HF metadata) or the catalog declares
  for the file URL (model + mmproj). If `size + headroom > available`, undo
  the caller's pre-download state, show one "This model won't fit on your
  disk" toast and return a typed `DownloadRefusal` instead of starting.
  Unknown size or free space, a failed command, or a resume all proceed;
  the Rust check stays the guard.
- Locale: `common:toast.downloadWontFit.{title,description}`.
- ADR: docs/decisions/2026-09-16-check-disk-space-before-a-download-starts.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(model-picker): keep a long provider title on one line in the list header

The provider group header in the composer's model picker gave the title
no width constraint, so "ChatGPT subscription (Codex)" wrapped to two
lines and pushed the selected dot and the gear button off their row.

The left group now takes the remaining width with `min-w-0 flex-1`, the
title span truncates with an ellipsis and carries the full text in its
`title` attribute, and the avatar, the dot and the gear are `shrink-0`.
The popover width is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(reply-gate): show the running download first, with the panel's Cancel

A model download started from onboarding or the Hub was invisible in the
blocked-send widget: it offered other models to download and said nothing
about the transfer under way, and the saved message was never sent when
that download finished.

The chat-model download in flight is now the first row of the widget's
list, in the same row shape as the recommendations, with the download
panel's readout (percent, bytes, time left; Paused; pre-byte stage) and a
secondary Cancel. Cancel runs the panel's branching, lifted into
`cancelDownload` in lib/downloadCancellation.ts; the status word is the
panel's, moved into lib/downloadFormat.ts as `downloadStatusLabel`. A
recommendation for the same file is not listed twice, and no other row is
promoted to "best fit" while the lead is downloading. Non-chat downloads
(embedding, projector-only, diffusion, backend, voice) are not listed. On
open with such a download the widget resolves with the new outcome
`download_in_flight`, so the composer arms the queued send and sends the
message when the model lands.

The now-dead `replyGate.downloading` / `downloadingPercent` strings are
removed from en, ja and ru.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): keep the effort setting away from messages already on screen

Dragging the reasoning-effort slider in the composer changed the transcript:
MessageItem subscribed to the global disableReasoning toggle and passed it to
buildTraceBlocks, which dropped a message's reasoning parts while the toggle
was on. A thread chatted with effort Off therefore grew a "Reasoned" block
above old answers the moment the slider moved, the content reflowed, and the
stick-to-bottom conversation container snapped down so the user's earlier
message scrolled out of view.

A message now renders the parts it actually has. buildTraceBlocks loses its
disableReasoning parameter, MessageItem no longer reads the general-setting
store, and the per-request decision (chat transport, agent reasoning request)
stays the only place the setting is applied. The MiniMax render-time
workaround is dropped; the trade-off is recorded in the ADR.

Tests: message-trace-parts keeps a stored reasoning part regardless of the
toggle; MessageItem.reasoning renders by message content and stays unchanged
when the store flips after render.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): explain why images need a vision model and offer the ones that run here

With a text-only model selected, "Add images" opened a small popover that
named one hard-coded model in untranslated English and never said what was
wrong. Replace it with a dialog that says the model in use can't see images
and lists the vision models this machine can run, each with Download (or
Use when already on disk), plus a row to the Hub for the rest of Hugging
Face.

- `useVisionDownloads`: the manifest's vision entries for the tier plus
  staff picks curated as `vision`, GGUF only, kept only when the card ships
  a projector, memory-gated (`wont_load` dropped), best fit first, deduped,
  capped at five. Modelled on `useRecommendedDownloads`.
- `VisionModelDialog`: `RouteRow`s like the reply-model gate; the import
  listener stays mounted so the composer still switches when a download the
  user closed the dialog on lands.
- `useStaffPicks` takes an optional predicate so the dialog resolves only
  vision picks instead of the whole curated list.
- Remove `PromptVisionModel` and the `JAN_V2_VL_*` constants; no model name
  lives in code any more.
- Locale keys under `chat:visionGate`; ADR
  2026-09-16-explain-why-images-need-a-vision-model.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(plugins): name catalog connectors by what they do instead of counting tools

The composer's Plugins menu listed every connector as "N tools", which says
nothing to someone deciding whether to switch it on, and an off connector had
no second line at all. Every catalog entry now carries a `taglineKey`
("Web search", "Google search", "Issues & projects"); the menu shows it under
the name for any server the catalog recognizes, on or off. The count / cost
line and the heavy warning move to the row's tooltip for those rows. Servers
the catalog does not know keep the count line as before, and "Off for this
chat" still wins while a connector is muted.

Adds English `mcp-connectors:taglines.*` for all 23 catalog entries and a
catalog test that every entry has a tagline that exists in the locale.

ADR: docs/decisions/2026-09-16-name-connectors-by-what-they-do.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(route-row): add a meta slot and a shared action class for the parallel UI fixes

Scaffolding for the onboarding polish batch: the fit badge needs a place
beside a row's title, and every action button in these lists should share
one class so the parallel fixes converge on one column. No behaviour change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(setup): word the onboarding route rows the way the product owner asked

The "Or get a model another way" rows on the onboarding screen — shared
with the composer's "Pick a model to start chatting" dialog — carried copy
Danny flagged on the 2.0.39 debug build: the Hugging Face row was titled
"Any model from Hugging Face" with a hint about GGUF and MLX builds, and
the cloud row's hint ("Use your OpenRouter, Anthropic, Gemini or OpenAI
key", 52 characters) overran the one clamped line a row gives it — about
260 px beside the mark and the width-reserving button under the 520 px
onboarding column, some 40 characters of 12 px Inter — and ended in an
ellipsis.

- huggingFaceTitle: "Any model from Hugging Face" → "Hugging Face models"
- huggingFaceHint / huggingFaceHintGguf: "Browse GGUF and MLX builds in
  Models" / "Browse GGUF builds in Models" → "Add any model" (both)
- huggingFaceTrigger: "Browse models on Hugging Face" → "Browse Hugging
  Face models"
- providerHint: "Use your OpenRouter, Anthropic, Gemini or OpenAI key" →
  "OpenRouter, Anthropic, Gemini, OpenAI"

English only; other locales fall back. The two component tests gain an
English-mode switch on their i18n mock and assert the rows in the words
the user reads, including the buttons' accessible names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(downloads): keep the panel row's readout on one line that never wraps

Danny's 2.0.39 test drive: the "downloaded GB" readout in the bottom-right
download panel wrapped onto a second line and the card shifted. The row's
readout was two flex spans and only the right one had `nowrap`; the row has
320 px, the worst realistic content needs 299.5 px at the default font size
(measured with the bundled Inter) and 336 / 372 px at the Large / Extra
Large settings, because `text-xs` scales with `--font-size-base` while the
panel's `22rem` does not. Past the limit the left span broke at its spaces,
the row grew, and the bottom-anchored card moved up.

The row now renders one `truncate tabular-nums` line — percent · downloaded
of total · time left — built by `formatDownloadReadout` in
`lib/downloadFormat.ts`, the same string the reply gate quotes. The parts
are ordered by importance, so a line that still cannot fit loses the tail of
the estimate to an ellipsis and never wraps. The transfer speed is no longer
shown in the panel; the estimate already folds it in. The panel keeps its
fixed `min(22rem, calc(100vw - 2rem))` width, which the one-line readout
fits at every font setting (225 px at Medium, 281 px at Extra Large).

Tests: the readout line, its classes, the paused and no-rate variants, the
name truncation, the controls slot, the panel's fixed width class with one
and three rows, and the collapsed badge. ADR recorded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(onboarding): ask before downloading a model that won't fit in memory

A red fit mark meant the file would download and then refuse to load,
but the row's Download button started the transfer regardless. Now a
red row asks first: "This model won't fit in memory", with the row's
name and the mark's own sentence in the machine's figures, Cancel as
the default answer and "Download anyway" running the click's original
body unchanged, so the memory question comes before the disk-space one.
Yellow and green rows, and rows with no verdict, start at once as
before.

One hook (useConfirmWontFitDownload) and one dialog
(ConfirmWontFitDownload), wired at both download lists: the onboarding
picker and the reply-model gate, which judges the file its row would
fetch through the same describeRecommendationFit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(setup): lay the onboarding rows out as one column

Danny's layout feedback on the first-run screen (64 GB M5 Max): the fit
marks drifted out of line across rows, the Download buttons were padded to
the width of "Downloading…", the size hung off the model name, the progress
readout shoved the button aside, and the route rows' marks were half the
size of the model logos.

- The fit mark is a badge with a word — Fits / Tight / Won't fit — in the
  source-badge style, on the name line; the name truncates, the badge never
  wraps under it. `ModelFitIndicator` keeps its props, accessible name and
  tooltip and translates the word itself (`setup:recommend.fitBadge*`).
- The size rides inside the button ("Download 2.5 GB"; plain "Download"
  when unknown; "Downloaded" as before).
- One width for every action button on the screen and in every `RouteRow`
  list: `ROUTE_ROW_ACTION_CLASS = 'min-w-[9.25rem] shrink-0 rounded-full
  px-3'`, sized for "Download 19.7 GB" measured in Inter Medium 14 px. The
  `RowActionLabel` width-reserve hack and `rowActionLabels` are retired.
- While a download runs the slot shows the same-width "Downloading… ×" pill
  and the readout takes the hint line under the name (`aria-live` kept); the
  offer keeps that line, empty, so its button never moves.
- `RouteRow`'s icon slot draws glyphs at 20 px and lets brand images fill
  the 32 px circle; the content column is 640 px instead of 520 px.

Tests: SetupScreen.test.tsx +5 (badge word and placement, size in the
button, one button per row with the readout under the name, one button
column without ghost labels, route mark sizing), 50 passing; ReplyModelGate,
PromptOnboardingModel and SetupScreenHelpers unchanged and green.

ADR: docs/decisions/2026-09-17-one-button-column-and-a-fit-badge-on-the-onboarding-rows.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(decisions): drop the duplicated total line and section header in the ADR index

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(reply-gate): list onboarding's full recommended list, marks and sizes included, and seat the folder route among the routes

The blocked-send widget showed three rows from the manifest's tail under no
label, with no fit mark, a bare "Download", a three-line subtitle and a
fill-less "Add a folder with models" link under the routes. Onboarding had
moved on to the offer plus the Hub's picks listed by fit and marked row by
row; the widget now shows that list, row for row, through a new
`useRecommendedListDownloads`: the same "Recommended models" label, the
same `ModelFitIndicator` in `RouteRow`'s meta slot, the download size on
the button ("Download 4.2 GB"), and a one-line subtitle. The folder route
is a `RouteRow` among the other routes ("Models on this computer / Pick a
folder and they're added / Add") and reads "Looking…" while it scans.

The pure pickers and the fit copy moved verbatim from SetupScreen.tsx to
SetupScreenHelpers.ts and are re-exported from the screen, so the hook can
share them without importing the screen and the registry stores it pulls in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(hub): make the Hugging Face feed a section heading, without the sort

On the Models page the feed under the picks began with a muted text-xs
caption reading "More from Hugging Face · trending now", which got lost
between rows; and the picks had had no heading of their own since the
filter bar was rebuilt, so the page did not read as two sections.

- The first pick row now carries a "Recommended" heading and the first
  feed row "More from Hugging Face", both an h2 in one base-size semibold
  style, painted above the row the way the caption was.
- The heading no longer names the sort: that is what the sort dropdown is
  for. `feedTitle` drops its `{{sort}}` interpolation and the orphaned
  `feedSort` labels go with it (English only; other locales fall back).
- The route test asserts both headings by role and level, their shared
  class, the English copy, and that search results stay heading-free.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): centre the approval rows' icons and keep the Full access warning to two lines

The composer's approval menu pinned each row's icon and checkmark to the
title line (`items-start` + `mt-0.5`), which put them at the top of the
70-86 px title + description block: on the three-line Full access row
they read as "flown up", and the fixed 2 px nudge sat above the title at
the Large and Extra Large font settings. The rows now use the menu
item's own `items-center`, so both sit on the middle of the block.

The "Enable Full access?" dialog wrapped its body to four lines at the
default width. It is now `sm:max-w-xl` (576 px, 526 px of text per line)
and the copy is 132 characters instead of 179, keeping every fact -
files modified or deleted, commands run, the internet used, only for a
task you trust - in two lines at the Medium and Large font settings
(three at Extra Large). Title and button labels are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(cloud): keep the provider column still when the scrollbar comes and goes

Switching providers on the Cloud page moved the whole content block a few
pixels sideways. A provider with a long model list scrolls, one with a
short list does not, and with classic (non-overlay) scrollbars — macOS with
a mouse connected, Windows, Linux — the bar's width came out of the scroll
container's content box, so the centred column was re-centred in a narrower
or wider box on every switch across that line.

Reserve the gutter on the route's scroll container (`scrollbar-gutter:
stable`, the rule SetupScreen already applies) so the box is the same width
whether or not the bar is showing. The cards themselves were already
identical across providers; a test now pins that too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(plugins): seed one web search and lay connector rows out like model rows

A fresh install's Plugins menu listed Serper switched off next to Exa
switched on: the same job, behind an API key the template shipped as a
placeholder, so it could never be turned on as seeded and was the only
off row the menu could show. Drop it from DEFAULT_MCP_CONFIG_TEMPLATE, the
connector catalog, its locale keys and its image; existing configs are
untouched (the template is written only when no file exists, and the
migrations only back-fill Jan Browser MCP and re-pin filesystem).

Rebuild the connector row on RouteRow's anatomy: a 32 px round mark that a
brand tile fills (a hand-added server shows its initial), a text-sm
font-medium title over a text-xs tagline, each truncated to one line and
centred on the mark, min-h-9 so rows without a tagline match their
neighbours, and a fixed 64 px action slot so the switches line up.

Tests: a Rust test asserts every off server in the default template is one
the menu hides; catalog tests assert Serper is gone and no locale key is
orphaned; DropdownPlugins tests assert the row's mark, title, tagline and
action slot. Resend takes over as the keyed-connector fixture.

ADR: docs/decisions/2026-09-17-seed-one-web-search-and-lay-out-connector-rows-like-model-rows.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ui): put the text fonts back in front of the emoji face so WebKit keeps Inter's strut

WebKit takes every line box's strut metrics from the first family's face even
when its unicode-range excludes U+0020, so the emoji-first stack from the
ATO-526 fix dropped every button label by 1 px on macOS (Apple Color Emoji's
23 px content area against Inter's 17 px). Inter and StudioFeixenSans now lead
their stacks and carve the emoji range out with a unicode-range of their own,
so ZWJ sequences still stay in one font. A test holds the two lists together.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* build(sidecar): bundle a pinned, verified cloudflared for Remote access

cloudflared becomes an externalBin sidecar on macOS, Windows and Linux. Unlike bun/uv it is pinned to a release (2026.9.1) and verified before it can reach a bundle: the release asset against the digest GitHub reports, and on macOS the executable inside the .tgz against the checksum in Cloudflare's release notes. macOS gets a real lipo of both slices. A stamp lets later runs skip the network; a missing, truncated or placeholder file, or a new pin, reinstalls from the re-hashed cache or downloads again.

make download-cloudflared (yarn download:cloudflared) runs only this step; yarn download:bin, the first step of every dev target and of the release jobs, includes it. CI checks the universal binary's architectures on macOS, the file on Linux, and on Windows that the sidecar still carries Cloudflare's valid Authenticode signature, which is kept on purpose.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(server): Cloudflare quick tunnel and dynamic trusted hosts for the Local API Server

Remote access: a manager with one supervisor task per run starts the bundled cloudflared, shows the URL only after it is registered and a public probe of /openapi.json (edge by SNI first, then the hostname) proves it reaches this server, retries once over HTTP/2, and reports every transition as remote-access:status. stop_server, RunEvent::Exit and restart_app take the tunnel down; a pid journal (plus PDEATHSIG on Linux) recovers an orphan after a crash. The user's own ~/.cloudflared/config.yml and TUNNEL_* variables are switched off for our process.

Host validation is unchanged. The proxy appends one dynamic group per request: the live tunnel's hostname and the local address of the accepted socket. That lets a tunnel and a LAN client through without Trusted Hosts input and cannot be forged by a DNS-rebinding page. get_lan_addresses lists dialable IPv4 addresses for display.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(settings): Remote & LAN page for the Local API Server

A desktop-only settings page with two cards. Remote access starts the Cloudflare quick tunnel (starting the server first when needed), shows the API base URL with copy and a QR code, and can start automatically whenever the server comes up. LAN access rebinds the server on 0.0.0.0 and lists the addresses to dial. The API key stays optional: starting Remote access without one asks once, offering a generated key; both cards warn while no key protects the API.

Tunnel status lives in an in-memory store fed by the remote-access:status event. Adds react-qr-code (approved), the four desktop-only commands to the IPC contract allowlist, en and ru copy, and tests for the helpers, hooks, store migration and the page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(decisions): expose the Local API Server through a Cloudflare tunnel and on the LAN

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(test): restore jsdom storage globals on Node 25 and newer

Node now owns localStorage and sessionStorage globals, so Vitest no longer populated them from jsdom. Repoint both globals during test setup before imports load.

Co-Authored-By: Codex <noreply@openai.com>

* fix(models): rebuild the empty model selector as a complete starting point

Show the full recommended list, provider routes, stable search results, and hide the redundant download shortcut when no models are available.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(connectors): give every catalog card one stable anatomy

Keep icons, text and actions aligned, equalize card geometry, and repair the Linear logo treatment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(composer): keep Plugins in the toolbar and retire its pin setting

The Plugins control is always available beside web search, leaves the attach menu, and old persisted visibility settings migrate away without muting connectors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(image-generation): enhance VAE tiling and job progress tracking

- Introduced `VAE_TILING_AREA` constant to manage VAE tiling based on output size, enabling efficient memory usage during image generation.
- Updated `build_img_gen_request` to include VAE tiling parameters when output dimensions exceed the defined area.
- Enhanced `ProgressTracker` to accurately track and log job progress, including handling of VAE tile announcements and sampled steps.
- Added tests to validate the new VAE tiling behavior and ensure correct progress tracking during image generation jobs.
- Improved error handling and logging for better diagnostics during server interactions.

* fix(upstream): recover installed backend before catalog wait

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(mcp): remove only the untouched retired Serper default

Migrate existing configs once while preserving every user-edited Serper entry and every other server.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(ui): add a real-browser layout gate

Measure overflow, wrapping, equal columns and text placement in headless Chromium with the shipped CSS and fonts, and document the layout rules agents must follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(models): simplify and stabilize model start snackbar

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(assistant): shorten default prompt and preserve custom instructions

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(models): distinguish 128 GiB recommendation tiers

Preserve legacy tier ids, add verified high-memory alternatives and vision projectors, and mirror the manifest in offline recommendations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* release: v2.0.40

* fix(onboarding): keep model selection empty after skip

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(search): show availability from discovered web tools

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ui): compact the model-loaded snackbar

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(composer): widen model settings popover

Give the model settings a viewport-bounded reading column, wrap engine status, and keep the effort slider full-width. Cover font scales, themes and viewport bounds in Chromium.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): widen approval menu and clarify full access copy

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(models): clear composer selection after explicit unload

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(onboarding): polish model rows and stable download actions

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(connectors): preserve rounded-square plugin icons

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ui): keep model loading snackbar copy stable

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): keep web search tooltip copy consistent

Keep the visible tooltip fixed while preserving accessible state labels and search retry behavior.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(models): widen selector and distinguish GGUF and MLX downloads

Keep search geometry stable, label supported formats, and separate provider settings controls from status dots.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(connectors): show actions according to configuration state

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): stabilize live reasoning viewport through completion

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(ui): isolate browser-layout optimization per worktree

Pre-optimize the path polyfill and keep Vite's browser cache inside each worktree so parallel layout runs cannot reload one another's React graph.

Co-Authored-By: Codex <noreply@openai.com>

* fix(chat): describe tool activity with friendly localized actions

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(chat): widen and rebalance the context popover

Give fit-to-memory copy room for three lines at Extra Large, clarify the type hierarchy, and keep the panel viewport-safe.

Co-Authored-By: Codex <noreply@openai.com>

* fix(projects): wait for files and expand the project composer

Avoid transient first-load errors, format structured failures, and let the project composer fill the card column with a taller input.

Co-Authored-By: Codex <noreply@openai.com>

* fix(agent): dock approvals to the composer surface

Align pending approvals with the input border, use a solid muted layer, and keep details and actions within stable bounds.

Co-Authored-By: Codex <noreply@openai.com>

* fix(chat): wrap code blocks inside padded bounds

Restore the chat code inset, wrap long highlighted lines without horizontal scrolling, and preserve copy and download source text.

Co-Authored-By: Codex <noreply@openai.com>

* fix(search): fall back when hosted Exa is unavailable

* fix(onboarding): compact row actions and hide tag dumps

* fix(models): share onboarding rows across empty states

* fix(downloads): make queued and cancelled states quiet

* fix(models): shorten technical names in the composer

* fix(sidebar): make Images a full-row section toggle

* fix(connectors): use the byline slot for status

* fix(models): use outlined active and unload icons

* test(updater): allow a build-only release preview

* test(updater): keep the local release preview visible

* fix(updater): give release notes more room

* fix(cloud): hide unsupported sampling controls

* fix(updater): stack above active downloads

* fix(connectors): report Exa fallback as usable

* fix(models): use real summaries for the lead model

* fix(models): compact and unify the model picker

* feat(chatgpt): expose model reasoning effort

* fix(models): clarify device fit badges

* fix(updater): avoid the welcome routes

* fix(models): make unload status concise

* fix(hub): show fit badges and restore selected quant

* fix(layout): recover collapsed side panels

* fix(reasoning): default required thinking to low

* fix(agent): tighten tool activity details

* fix(models): tighten selector feedback and motion

* fix(models): standardize load feedback

* fix(models): standardize load feedback

* fix(chat): unify reasoning and agent activity

* fix(search): restore keyless search and fetch

* fix(search): polish web result presentation

* fix(dialog): dismiss connector tools outside

* fix(ui): standardize secondary buttons

* fix(chat): stabilize streaming code blocks

* fix(projects): initialize empty file collections

* fix(hub): paginate uncensored models

* fix(update): portal banner above route content

* fix(sidebars): snap panels and align toggles

* fix model download handoff

* fix run settings reset controls

* fix full reasoning stream layout

* fix vision model dialog layout

* fix skill recency and delete copy

* Revert "Merge branch 'fix/manifest-tiers-128' into danny/image-generation-fixes"

This reverts commit 810b692e79a6dffdedaee9a4e73aea45654d4ad5, reversing
changes made to 82bcd8daa52967bbba4e156984e40c62ce9ffe89.

* fix final model picker regressions

* fix: polish onboarding agents and image generation

* fix(images): keep bundled catalog additions available

* test(updater): add full QA update preview flow

* fix(updater): retire preview banner after install

* fix(images): expose model start and stop controls

* fix(images): streamline model and workflow UX

* chore(diffusion): point the bundled sd.cpp manifest at the signed mirror

mirror-sdcpp.yml ran for master-849-d04e895 and moved
atomic-chat-conf/backends/sdcpp-manifest.json to the conf release
(download_base), with the sha256/size of the re-signed macOS and Windows
archives. Regenerated with `make sync-upstream-baseline` so the offline
fallback installs the same signed builds instead of leejet's originals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: complete image generation and QA flows

* fix: polish image and model workflows

* fix: refine chat activity and image workflows

* fix: refine chat selection and agent activity UX

* feat(images): finalize Qwen 2.1 runtime and generation UX

* feat(dev): support isolated Atomic Chat profiles

* fix: avoid update banner overlap on first-run setup

* fix: measure update banner after portal mount

* fix: keep onboarding below update notifications

* fix(images): default Qwen 2.1 to 1024px

* feat(images): add Krea 2 Turbo support

* refactor: update reasoning effort handling for Gemini and Google providers

- Adjusted reasoning effort logic in CustomChatTransport to ensure compatibility with Gemini models, specifically handling different reasoning levels based on model ID.
- Updated cloud reasoning request fields to remove unnecessary `thinking_config` for Gemini, which now only accepts `reasoning_effort`.
- Enhanced tests to validate new reasoning effort behavior for Gemini and Google, ensuring correct responses for various input levels.

* release: v2.0.41

* feat(zcode): integrate ZCode as a launch-page coding agent

- Added ZCode as a new coding agent, allowing users to launch it directly from the application.
- Updated README to include ZCode in the one-click agent launch section.
- Created a new decision document detailing the integration process and configuration for ZCode.
- Implemented necessary changes in the Tauri backend to support ZCode's provider configuration and launching.
- Added UI elements and localization for ZCode in the web application, including toast notifications for installation status.
- Included tests to ensure proper functionality of the ZCode integration.

* release: v2.0.42

* feat(images): open the studio once the engine is installed; models come from its picker

The first-run screen stayed up until a model was on disk too, so with the
engine installed and a checkpoint downloading the page still showed the
setup card. The engine is now the only gate:

- no engine: the setup card, as before (centered, or beside existing images)
- engine installed: the studio, with or without a model; the empty canvas
  offers "Download model", which opens the form's model picker
- the wizard tour is intro -> engine -> Done; the model list stays as a
  standalone view for Media settings, which has no picker of its own
- the "download" error action opens the picker instead of the wizard

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(images): a model is complete without the optional file its download never fetches

listInstalledArtifacts counted every plan entry that is not on disk as
missing, including Qwen-Image-2.1's optional llm_vision projector.
downloadArtifact fetches required entries only, so a freshly downloaded
Qwen-Image-2.1 stayed `complete: false` forever: the row said Downloaded
(it reads the workflow plan) while the wizard's Done stayed disabled and
the page kept treating the model as absent.

Completeness now follows the same required set the download uses. Edit and
Reference still ask for the projector through their own workflow plan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(images): draw Krea 2 Turbo by name with the mark its id already has

The family id `krea-2-turbo` resolves to the `bfl` mark, as FLUX.1 Krea
does, but no name rule matched "Krea 2 Turbo", so a card drawn from the
family name showed no mark. A `krea 2` rule now gives it the same one.

The catalog test asserted only that some name rule matched; it now
requires the name's mark to equal the id's for every family, the
mismatch this was. Checked against model-logo.ts without the new rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 8647144f2ebfc5b0e66133d51e75fc6d19494ea7)

* test(registries): refresh the recommended-models fixture with the 128 GiB tiers

The client knows vram_128, vram_128_plus, unified_128 and
unified_128_plus, and the published manifest lists them since
atomic-chat-conf 34ecae48, but the fixture was still the 2026-09-11
copy, so tests/registry-contracts.test.mjs failed on the missing tiers.
The fixture is now a verbatim copy of models/recommended.json at that
commit; sources.json records where it comes from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 254c29a6a4b8acb511196c26462f457951026d9f)

* release: v2.0.43

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Danny Yurkin <283456650+danyurkin@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Brings the image first-run flow and the completeness fix from the
image-generation line:

- the engine is the only gate: the setup card shows while it is missing,
  then the studio opens and models come from the form's picker
- a model is complete without the optional llm_vision projector its
  download never fetches
- release: v2.0.43

The Krea 2 Turbo logo rule and the 128 GiB tiers fixture were already
here; the image line took them from this one.

Nothing to change in atomic-chat-core: it has no catalog and never decides
whether an artifact is complete, and it already treats llmVision as
optional (Qwen Image 2.1 without it only creates).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…on toast that never closed (#310)

* fix(downloads): one writer per file, and cancellation that does not wait for a chunk

A field log shows a diffusion download paused and resumed within the same
second on a slow link. The paused streams only noticed their cancellation
when the next chunk arrived, eight seconds later, while
`cancel_download_task` had already freed the task id, so two invocations
had one `.tmp` open. A late `File::create` from the old one truncated the
partial the new one was writing: a VAE of the right size whose sha256
failed after a two-hour download.

- Process-wide lock per save path, taken before the partial's state is
  read; waiting for it is cancellable.
- Cancellation is raced with `stream.next()` and with the request, and
  flushes the writer so the partial's length is final before the lock
  is released.
- No file is created for a task cancelled while its response was pending.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(images): close the "verifying" toast on failure and say why the download failed

The Rust downloader opens the "Verifying the downloaded files…" toast
with no timeout, and only a success or `onModelValidationFailed` closed
it. Image model downloads reported a failed size/sha256 check as an
ordinary transfer error, so the file was removed, a generic toast flashed
by, and the spinner stayed forever.

- `downloadArtifact` reports a failed integrity check as
  `onModelValidationFailed`, like the llama.cpp extensions do.
- The panel closes the verification toast on any transfer error or stop.
- Two artifacts that share a side file no longer fetch it twice at once:
  the second leaves it to the transfer that has it, and fetches it itself
  if that one fails.
- A resume from the download panel keeps the current workflow and
  re-lists the model files when it finishes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Updated button class names in the AddEditMCPServer component to ensure proper styling in dark mode. The changes include adjustments to hover states and background colors for both local and remote connection buttons, improving visual consistency and user experience across themes.
a5ff5f8 (Feat/image generation sdcpp #300) has the same tree as
d5669b0, the feat/image-generation-sdcpp tip merged in c11c4c0.
Recording it with -s ours changes no file; it only makes it the merge
base for main's later commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings the download fixes of #310, the MCP dialog's dark-mode buttons
and the v2.0.44 version bump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Vect0rM commented Sep 22, 2026

Copy link
Copy Markdown
Member Author

Second pass, on 3439bde. Of the five things from last round, three are closed — and the two that aren't are the same two I'd still like before merge, plus one new one the repo's own gate found.

The headline: base (835f663) is now an ancestor of the head, so this isn't a merge at all — it fast-forwards feat/atomic-chat-core-migration by 281 commits. git merge-tree reports 0 conflicts, against 21 last round.

Verified

Measured on the PR head and on the base branch in the same container.

One note for anyone reproducing, because it cost me a run: yarn test needs yarn build:core first. Without core/dist, 106 test files fail to collect on Failed to resolve entry for package "@janhq/core" and it looks like the branch is on fire. It isn't — with core built it's clean.

  • make typecheck — exit 0.
  • yarn lint — 0 errors, 30 warnings (all react-refresh/only-export-components and react-hooks/exhaustive-deps).
  • Full yarn test — 390 files passed, 1 skipped; 4508 passed, 16 skipped. Exit 0. I ran the base-branch baseline in the same container rather than quoting last round's numbers: 311 files / 3201 passed. So +79 test files and +1,307 tests, nothing else disturbed, nothing red.
  • No CI, stillget_check_runs on 3439bde returns total_count: 0, fourth round running.

Two things I checked because they're the ones that fail silently, and both are right:

The squash-merge-back didn't eat anything. This branch carries #300's original commits and merges main's squash of the same work (21a2896). git diff 21a2896^1 21a2896 is empty — the squash brought in nothing and dropped nothing. That's the correct resolution of the case that usually reverts things without anyone noticing.

c537970 survived main landing on top of it. 3439bde rewrote 141 lines of web-app/src/lib/diffusion/models.ts from main, over the same function c537970 had just fixed. The .filter((e) => e.required && !e.present) is still there at line 290 — I checked the line, not just that the file was touched.

Closed since last round

1. The merge, and the e2e suite. 1d836ae Reapply "Feat/app e2e harness" undid the revert. All the files I was worried about are on the head: 40 under tests/e2e/, plus src-tauri/src/core/e2e.rs and src-tauri/tauri.e2e.conf.json. Nothing got resolved by deletion.

2. The sdcpp pin. 47525cb is in.

5. The Remote & LAN split. Settled by #300 landing on main as a5ff5f8.

Still open

3. The dist-js files are still there

Unchanged — same four files, same commit (09778f0), still no Cargo.toml, no src/, no package.json, no .gitignore:

src-tauri/plugins/tauri-plugin-foundation-models/dist-js/index.cjs    2,702 B
src-tauri/plugins/tauri-plugin-foundation-models/dist-js/index.d.ts   1,475 B
src-tauri/plugins/tauri-plugin-foundation-models/dist-js/index.js     2,296 B
src-tauri/plugins/tauri-plugin-foundation-models/dist-js/types.d.ts     187 B

Base has zero files under that path. main has the plugin as a 26-file crate. This branch is the only place the compiled output exists, without the source it was compiled from.

One new detail that makes it worth deleting rather than shrugging at — the repo has a test asserting this plugin is gone, tests/desktop-legacy-path.test.mjs:

test('the Foundation Models plugin is gone', () => {
  assert.throws(() =>
    statSync(join(ROOT, 'src-tauri', 'plugins', 'tauri-plugin-foundation-models', 'src'))
  )

It passes — because it probes src/, and dist-js/ is what survived. So we assert the plugin is gone while shipping its build output.

I did check whether it breaks anything before asking again: src-tauri/plugins/package.json globs **, which only matches directories containing a package.json, so the plugin build skips it. It's dead weight, not a hazard. git rm -r the directory.

4. The description is still the template

## Describe Your Changes

-

666 files, 281 commits, three unchecked boxes — and since this fast-forwards the integration branch, it's the record for the whole line. Most of what it needs is in this comment and the last one.

New: make verify-fast fails, and it's the pre-push hook

.husky/pre-push runs make verify-fast. On this head it fails:

node scripts/check-test-quality.mjs
Test-quality guard found new false-confidence patterns:
- [call-only-assertions] web-app/src/containers/__tests__/DownloadManagement.validation-toast.test.tsx

Both tests in that file assert only that a mock was called — toast.loading at :78 and toast.dismiss at :104 — with no observable outcome, and the file isn't in tests/test-quality-allowlist.json.

Worth saying where it came from, because it reflects well on the branch rather than badly: it arrived from main at 41ae3a8 in the v2.0.44 merge. I ran the same guard on origin/mainmain fails it on 9 files; this branch is down to 1, because 09f6fcc, bf1605b and a543b9d went back and converted the others. This is the one straggler that landed after that sweep.

09f6fcc is the pattern to copy — one real assertion alongside the mock one (store state, or the DOM) is all the guard wants. Allowlisting it would also close the gate, but the branch has earned the better fix everywhere else.

For the record, the other two verify-fast targets that failed here are my environment, not the branch: test-hardening-contracts fails 6 of 60 on ENOENT for resources/bin/jan-cli and resources/pre-install, and test-coverage-critical dies on Couldn't find the node_modules state file because I never ran yarn install inside extensions/. Both need make install-and-build, which I can't complete here. test-telemetry-props passes.

Checked and fine

  • The telemetry consent gate. TELEMETRY_ENABLED defaults to true, which I went looking at sideways for a privacy-first app — but the comment's claim holds: useAnalytic.ts:64 defaults productAnalytic: true, so the Rust gate matches rather than overriding. AnalyticProvider's effect pushes the persisted value on mount, the core is launched --telemetry on|off from the single consent_enabled() source, and only the desktop supervisor ever calls launch_and_wait — so the default-on window is bounded by webview mount, exactly the window the ADR says Rust panics already have. Documented, precedented, and the ADR is unusually good about it.
  • ZCode. Patching a third-party agent's config file is already house practice (openclaw_patch_config), the ADR names Apache-2.0, and the lock/backup/refuse-don't-repair handling is more careful than it needed to be. zcode.png is the 13th bundled integration mark, so no new licensing question — I checked the folder before raising one.

Nits

  • Prettier. 256 of the 602 changed text files fail --check. Before treating that as a number: 77 of them fail identically on the base branch, and prettier is enforced nowhere — not in .husky/pre-commit (yarn lint --fix --quiet), not in verify-fast, not in CI. So it's house-wide drift, not a gate. What's actually this branch's: 23 files that are clean on base and dirty on head, which is the list worth a prettier --write:
    web-app/src/lib/messages.ts
    web-app/src/lib/agent-file-links.ts
    web-app/src/containers/RenderMarkdown.tsx
    web-app/src/containers/SetupScreen.tsx
    web-app/src/hooks/useAgentApprovalActions.ts
    …and 18 more
    
    The other 157 are new files, take them or leave them.
  • Two commits are in twice. 254c29a/8b20b0b and 8647144/bab21a3 are pairs with identical patch-ids — cherry-picked onto this line and also present on the sdcpp line, joined at c11c4c0. I checked the result rather than assuming a clean join: model-logo.ts has one Krea rule, recommended-models.json parses with no duplicate keys. History noise, nothing to fix.
  • yarn test still doesn't reach extensions/. Root vitest.config.ts lists only ./core and ./web-app as projects, so 26 test files under extensions/ never run in the root suite — 4 of them new here, including both loadCancel suites. Same gap as the last two rounds, now with more tests sitting behind it.

Delete the dist-js directory, give that one test an observable outcome, and write the description — after that I have nothing. The consent plumbing and the ZCode config merge are both better documented than they needed to be 🧩


Generated by Claude Code

Vect0rM and others added 9 commits September 22, 2026 13:05
The core now owns its error reporting and reports by default unless its host
says off. The app passes `consent && this build reports` (telemetry::core_consent)
as the launch flag and in every PUT /telemetry, so a `tauri dev` session or a
test build never lets the core reach the production project; make
test-core-live runs the core in the development environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Added a new function, `stopModelsBeforeUpdate`, to ensure local models are unloaded before initiating an update. This function includes a timeout mechanism to handle cases where unloading may stall, allowing the update to proceed even if unloading fails. Updated the `useAppUpdater` hook to utilize this new function. Enhanced tests to verify that updates can still be downloaded when unloading local models fails or times out, ensuring a smoother update experience for users.
…e dead URL

Every launch of the app fetches `models/diffusion.json` and
`backends/sdcpp-manifest.json` from GitHub (`ImageGenerationProvider` binds at
the root), so until now every desktop e2e scenario reached the real network.
The e2e build now bakes the dead address for both, like the other registries;
a scenario that needs them seeds the webview's one-hour cache instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`bundled_cloudflared()` always handed the core the real `cloudflared` next to
the app binary, and the core prefers that flag over any environment: a remote-
access scenario would have opened a public Cloudflare tunnel to the test's API.
An e2e build now passes only a `cloudflared` the run put next to its scripted
sidecars (`<root>/sidecars`, the mechanism `sidecar_resources_dir` already
uses), or none — the core then reports `cloudflared_unavailable`.

`launch_zcode` would have opened a real ZCode on the machine of whoever runs
the tests; like `open_agent_terminal`, an e2e build writes down what it would
have opened and reports it as launched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The gallery store loads once and then only prepends what the page's own run
loop produced; the store's `job` event handler ignored a terminal job it had
no waiter for. A picture an outside client made through
`POST /v1/images/generations` — or another window — landed in the core's
gallery and appeared only after a restart. A completed job nobody here waits
for now prepends its outputs, the way the run loop does for its own; a job of
the page's own is still shown once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`releaseGpuForChat` — the chat side of the arbitration the 2026-09-10 record
decided — was defined, tested and called from nowhere: `startModel` went
straight to `engine.load`, so a chat model loading beside a resident image
model competed with it instead of evicting it. `startModel` now calls
`makeRoomForChatModel` first, which asks the engine for the model's size and
hands it to the arbiter (an unknown size claims infinity, the arbiter's own
"does not fit"); a no-op without image generation, never the reason a load
fails. ADR 2026-09-22.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Incremented the record count in the decision index from 348 to 349. Added a new entry for the decision made on 2026-09-22 regarding the desktop e2e image engine and catalog. Updated the section header for inference core extraction to reflect the new record count.
Implemented a mechanism to keep the e2e window rendering even when it is not visible, specifically addressing issues with full-screen applications on macOS. This change utilizes a private WebKit API to disable window occlusion detection, ensuring that the e2e tests do not time out when the window is obscured. Updated the `create_windows` function to include this new behavior and added a conditional function for macOS to manage the rendering state.

Also updated the `Cargo.toml` and `Cargo.lock` files to include the `objc2` dependency for macOS-specific functionality.

Additionally, modified the `endSession` function in the e2e harness to handle undefined session cases gracefully.
Updated the MCP tool approval label in the composer to display only "Ask for approval" without the previous caveat of "MCP tools auto-approved." This change aligns with the product's decision to simplify the user interface and clarify the approval process for MCP tools. Adjusted related tests to reflect this modification and ensure consistent behavior across the application.
@Vect0rM
Vect0rM merged commit c8227ea into feat/atomic-chat-core-migration Sep 22, 2026
Vect0rM added a commit that referenced this pull request Sep 23, 2026
The base, feat/core-migration-image-generation, landed as a squash (#303),
so every file outside this PR takes the target's version.

- hub-download: keep both the refusal scenarios and the retry scenario,
  and point the retry scenario at "Model Hub", the sidebar's label now.
- fixtures: the Hub fixture takes both sizeLabel and failFirst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants