mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 11:21:34 +00:00
* feat(ui): surface queued-outbox counts in composer hint, session rows, and offline footer
Follow-up to the offline-state unification: queued sends were mechanically
safe (durable outbox with reconnect replay) but only visible inside the
open session's thread.
- Composer offline hint now includes the visible session's queued count.
- Session rows show a clock badge with the per-scope outbox count,
independent of connection state (covers waiting-idle and failed too),
with alias-safe scope resolution so agent-main never double-counts.
- The offline footer button appends the aggregate ("· N queued"); the
connected state stays completely silent.
- One narrow subscription seam (subscribeStoredChatOutboxChanges) added
in composer-persistence; no send/drain logic touched.
* perf(ui): keep the startup bundle under budget with a lean outbox read module
The queued-count feature statically imported composer-persistence from
startup modules, hoisting the chat page's persistence machinery into the
startup chunk and breaking the Control UI startup JS gzip budget
(319.6 KiB > 314.0 KiB limit).
Split ownership instead of gaming the budget: a lean read/subscribe
module (ui/src/lib/chat/outbox-store.ts + codec/draft-state) serves
startup consumers (app-host, sidebar), while writes, migrations, and
drain stay in the lazy chat chunk (composer-outbox-store/composer-storage);
composer-persistence keeps its export surface for chat callers. Startup
is back to 313.8 KiB gzip at 12 requests with no chunking-config changes.
Also fixes an autoreview finding in the new summary: legacy bare-main
outbox rows now resolve through session defaults (online) or the
persisted mainAlias (offline reload) instead of trusting the row's stale
embedded agent id, so badge counts key to the same scope the sidebar
resolves. The shared footer status renderer is deduplicated into
session-row-badges.
* fix(ui): correct type-only import and const tuple in outbox split
* perf(ui): idle-load the outbox summary so startup carries no outbox code
The lean outbox read module still cost ~2.8 KiB of startup gzip against
1.5 KiB of budget headroom. Follow the sidebar chrome pattern
(lobster-pet/facepile): app-host idle-loads outbox-store, subscribes on
arrival, and passes the sidebar a resolver callback instead of letting
startup modules import scope resolution. Badges and counts hydrate
moments after load; before that the summary is empty by design.
Failed chunk loads recover on browser online events and, because chunks
are usually served by the gateway itself, on gateway reconnect — the
exact moment the offline badges become relevant again.
Raise the initial-graph packing ceiling 448->512 KiB: the grown core
graph split at the old boundary into an extra chunk, costing ~1.9 KiB of
startup gzip to compression-context resets (same documented tradeoff as
the earlier 400->448 bump). Startup lands at 313.5 KiB gzip / 9 requests,
matching the origin/main baseline, limit 315.0.
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
// Control UI config module wires control ui chunking behavior.
|
|
function normalizeModuleId(id: string): string {
|
|
return id.replace(/\\/g, "/");
|
|
}
|
|
|
|
function moduleIdIncludesPackage(id: string, packageName: string): boolean {
|
|
const normalized = normalizeModuleId(id);
|
|
return (
|
|
normalized.includes(`/node_modules/${packageName}/`) ||
|
|
normalized.includes(`/openclaw-pnpm-node-modules/${packageName}/`)
|
|
);
|
|
}
|
|
|
|
export function controlUiStableChunkName(id: string): string | undefined {
|
|
const normalized = normalizeModuleId(id);
|
|
|
|
// These entry-and-route helpers must stay together; separate shared chunks
|
|
// turn small route-graph changes into extra startup preload requests.
|
|
if (
|
|
normalized.endsWith("/ui/src/components/config-form.shared.ts") ||
|
|
normalized.endsWith("/ui/src/lib/clipboard.ts") ||
|
|
normalized.endsWith("/ui/src/build-info-normalizers.ts") ||
|
|
normalized.endsWith("/ui/src/build-info.ts")
|
|
) {
|
|
return "control-ui-shared";
|
|
}
|
|
|
|
if (normalized.endsWith("/ui/src/lib/gateway-methods.ts")) {
|
|
return "gateway-runtime";
|
|
}
|
|
|
|
if (
|
|
moduleIdIncludesPackage(id, "lit") ||
|
|
moduleIdIncludesPackage(id, "lit-html") ||
|
|
moduleIdIncludesPackage(id, "@lit/reactive-element")
|
|
) {
|
|
return "lit-runtime";
|
|
}
|
|
|
|
if (
|
|
moduleIdIncludesPackage(id, "highlight.js") ||
|
|
moduleIdIncludesPackage(id, "markdown-it") ||
|
|
moduleIdIncludesPackage(id, "markdown-it-task-lists") ||
|
|
moduleIdIncludesPackage(id, "dompurify") ||
|
|
moduleIdIncludesPackage(id, "entities") ||
|
|
moduleIdIncludesPackage(id, "linkify-it") ||
|
|
moduleIdIncludesPackage(id, "mdurl") ||
|
|
moduleIdIncludesPackage(id, "punycode.js") ||
|
|
moduleIdIncludesPackage(id, "uc.micro")
|
|
) {
|
|
return "markdown-runtime";
|
|
}
|
|
|
|
if (
|
|
moduleIdIncludesPackage(id, "zod") ||
|
|
moduleIdIncludesPackage(id, "json5") ||
|
|
moduleIdIncludesPackage(id, "libphonenumber-js")
|
|
) {
|
|
return "config-runtime";
|
|
}
|
|
|
|
if (
|
|
moduleIdIncludesPackage(id, "@noble/ed25519") ||
|
|
moduleIdIncludesPackage(id, "@noble/hashes") ||
|
|
moduleIdIncludesPackage(id, "ipaddr.js")
|
|
) {
|
|
return "gateway-runtime";
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export const controlUiCodeSplitting = {
|
|
includeDependenciesRecursively: false,
|
|
groups: [
|
|
{
|
|
name: (id: string) => controlUiStableChunkName(id) ?? null,
|
|
test: (id: string) => controlUiStableChunkName(id) !== undefined,
|
|
priority: 20,
|
|
},
|
|
{
|
|
name: (id: string) =>
|
|
normalizeModuleId(id).includes("/ui/src/") ? "control-ui-core" : "control-ui-foundation",
|
|
tags: ["$initial"] as ["$initial"],
|
|
priority: 10,
|
|
// 512 KiB packs the grown core graph into fewer chunks; the previous
|
|
// 448 KiB boundary split one core chunk in two, costing ~1.9 KiB startup
|
|
// gzip (same tradeoff as the earlier 400->448 bump).
|
|
maxSize: 512 * 1024,
|
|
},
|
|
],
|
|
};
|