Finish the mobile performance pass

- Cache the model catalog once per user and each conversation's setting
  separately; clear both on logout and after model saves
- Compress hashed assets asynchronously with a byte budget and set Vary
- Memoize message parsing and throttle selection reads
- Debounce draft writes to localStorage
- Keep the composer editor on the page and focused, as before
- Add a bundle budget check and run it in CI
- Show a loading block while sessions load and a workspace avatar in the
  mobile conversation header
This commit is contained in:
2026-10-20 14:37:11 +01:00
parent c1a94fc9a5
commit 4f7e0a6bf0
38 changed files with 1320 additions and 334 deletions
@@ -0,0 +1,84 @@
import type {
HarnessCatalog,
HarnessOverview,
HarnessSettingOverview,
} from "@/src/types";
type Harnesses = {
harnesses: HarnessOverview[];
};
/**
* The install-wide part of a model overview: everything except the bot's
* own settings.
*/
export function harnessCatalogFromOverview({ harnesses }: Harnesses): HarnessCatalog[] {
return harnesses.map(({ harness, title, description, required, automatic, providers, models }) => ({
harness,
title,
description,
required,
automatic,
providers,
models,
}));
}
/**
* A bot's (or workspace's) part of a model overview: the settings, what
* runs now, and what the default runs on. A picked model or provider the
* catalog no longer lists travels with it, so it isn't lost.
*/
export function harnessSettingsFromOverview({ harnesses }: Harnesses): HarnessSettingOverview[] {
return harnesses.map(({ title, description, required, automatic, providers, models, ...setting }) => {
const pinned_model = setting.model_id
? models.find((model) => model.id == setting.model_id)
: undefined;
const unavailable_provider =
setting.provider_id && !providers.some((provider) => provider.id == setting.provider_id)
? { id: setting.provider_id, name: "Unavailable provider" }
: undefined;
return {
...setting,
...(pinned_model ? { pinned_model } : {}),
...(unavailable_provider ? { unavailable_provider } : {}),
};
});
}
/**
* A catalog and a conversation's settings as the models form wants them:
* the catalog's list of models and providers under the setting. Undefined
* until both parts are there.
*/
export function mergeHarnessOverview({
catalog,
settings,
}: {
catalog?: HarnessCatalog[] | null;
settings?: HarnessSettingOverview[] | null;
}): HarnessOverview[] | undefined {
if (!catalog?.length || !settings?.length) return undefined;
const by_harness = new Map(catalog.map((entry) => [entry.harness, entry]));
return settings.flatMap((setting) => {
const entry = by_harness.get(setting.harness);
if (!entry) return [];
const { pinned_model, unavailable_provider, ...rest } = setting;
const models =
pinned_model && !entry.models.some((model) => model.id == pinned_model.id)
? [...entry.models, pinned_model]
: entry.models;
const providers =
unavailable_provider &&
!entry.providers.some((provider) => provider.id == unavailable_provider.id)
? [...entry.providers, unavailable_provider]
: entry.providers;
return [{ ...entry, ...rest, models, providers }];
});
}