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
@@ -1,50 +0,0 @@
import { AppData } from "@/src/data/app-data";
import type { HarnessOverview } from "@/src/types";
/**
* Bumped when `HarnessOverview` changes shape, so an older cache is
* fetched again instead of read.
*/
const CacheVersion = 1;
type Key = {
bot_id: number;
workspace_id?: number | null;
};
export type CachedHarnessOverview = {
v: number;
/**
* Unix ms it was fetched.
*/
at: number;
harnesses: HarnessOverview[];
};
function cacheKey({ bot_id, workspace_id }: Key) {
return `${AppData.HarnessOverviewStorageKey}:${bot_id}:${workspace_id || 0}`;
}
/**
* The models modal's data last fetched for this conversation, from
* `localStorage`; null when there is none or it can't be read.
*/
export function readHarnessOverviewCache(key: Key): CachedHarnessOverview | null {
try {
const cached = JSON.parse(localStorage.getItem(cacheKey(key)) || "null");
return cached?.v == CacheVersion &&
Array.isArray(cached.harnesses) &&
typeof cached.at == "number"
? cached
: null;
} catch {
return null;
}
}
export function writeHarnessOverviewCache({ harnesses, ...key }: Key & { harnesses: HarnessOverview[] }) {
try {
localStorage.setItem(cacheKey(key), JSON.stringify({ v: CacheVersion, at: Date.now(), harnesses }));
} catch {}
}
@@ -0,0 +1,103 @@
import { describe, expect, test } from "bun:test";
import type { HarnessOverview } from "@/src/types";
import {
harnessCatalogFromOverview,
harnessSettingsFromOverview,
mergeHarnessOverview,
} from "./harness-overview-parts";
const overview: HarnessOverview = {
harness: "text",
title: "Text",
description: "Chats.",
required: true,
automatic: true,
model_id: 4,
provider_id: 1,
providers: [{ id: 1, name: "OpenRouter" }],
fallback: false,
enabled: true,
options: { voice: "alloy" },
models: [
{
id: 4,
provider_id: 1,
provider_name: "OpenRouter",
slug: "deepseek",
name: "DeepSeek",
free: true,
},
],
current: {
id: 4,
provider_id: 1,
provider_name: "OpenRouter",
slug: "deepseek",
name: "DeepSeek",
free: true,
source: "pinned",
},
candidate_count: 2,
inherit: false,
};
describe("harness overview parts", () => {
test("splits the install-wide catalog from the conversation's setting", () => {
const [catalog] = harnessCatalogFromOverview({ harnesses: [overview] });
const [setting] = harnessSettingsFromOverview({ harnesses: [overview] });
expect(catalog).toMatchObject({
harness: "text",
title: "Text",
providers: overview.providers,
models: overview.models,
});
expect(setting).not.toHaveProperty("models");
expect(setting).not.toHaveProperty("providers");
expect(setting).toMatchObject({
harness: "text",
model_id: 4,
provider_id: 1,
fallback: false,
enabled: true,
});
});
test("merges a cached catalog and setting back into the form's shape", () => {
const [catalog] = harnessCatalogFromOverview({ harnesses: [overview] });
const [setting] = harnessSettingsFromOverview({ harnesses: [overview] });
expect(
mergeHarnessOverview({ catalog: [catalog!], settings: [setting!] }),
).toEqual([overview]);
});
test("waits for both parts", () => {
const [catalog] = harnessCatalogFromOverview({ harnesses: [overview] });
expect(mergeHarnessOverview({ catalog: [catalog!], settings: [] })).toBeUndefined();
expect(mergeHarnessOverview({ catalog: [], settings: [] })).toBeUndefined();
});
test("keeps a picked model the catalog no longer lists", () => {
const [catalog] = harnessCatalogFromOverview({
harnesses: [{ ...overview, models: [] }],
});
const [setting] = harnessSettingsFromOverview({ harnesses: [overview] });
const [merged] = mergeHarnessOverview({ catalog: [catalog!], settings: [setting!] })!;
expect(setting?.pinned_model).toEqual(overview.models[0]);
expect(merged?.models).toEqual(overview.models);
});
test("keeps a provider the catalog no longer lists", () => {
const without_providers = { ...overview, providers: [], models: [] };
const [catalog] = harnessCatalogFromOverview({ harnesses: [without_providers] });
const [setting] = harnessSettingsFromOverview({ harnesses: [without_providers] });
const [merged] = mergeHarnessOverview({ catalog: [catalog!], settings: [setting!] })!;
expect(merged?.providers).toEqual([
{ id: 1, name: "Unavailable provider" },
]);
});
});
@@ -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 }];
});
}