UI ownership rules here; repo-global architecture, proof, Git rules in root AGENTS.md.
- Lit 3/Web Awesome ports + Solid 2 components/projections: follow Solid skill + assigned migration order for conversion, lifecycle, interop, proof. State ownership below still applies.
- Final Lit sweep: remove Lit-specific guidance.
Solid route modules expose renderSolid(props) from the existing lazy page import.
The outlet mounts that renderer once and supplies live data, loaderPending, and
presented props; loader updates must preserve page-local state. Unported route
modules keep render. Keep page imports inside their route's component loader.
- Gateway owns shared state; renderer copies = caches; views own presentation state. Reuse owning store/controller + Gateway contract for session, config, authorization decisions.
- Scope caches/requests to connection, agent, session. Before publishing: check owner + request generation; old results must not overwrite newer intent/another context.
- Optimistic updates: retain visible recovery state + reconcile with authority. Old requests must not roll back newer edits. Failed writes follow Gateway target and outcome contract, never renderer-selected fallback account/connection.
- Background updates may refresh scoped cache, never replace foreground selection/publish another context's state.
src/lib/sessions/session-list-query.tsowns held-window reconciliation. Lifecycle, participants, placement, patch/send/steer, run-start/settlement/capacity, title snapshots may skip list reads only with unchanged membership, lineage, pin/owner/archive facts + nondecreasing recency. Tree events: complete access-scopedancestorSessions+ anyancestorSessionRefs; references need held row's admitted content revision. Preserve each row's generation + field receipts. Certified nested rows own facts; only explicit null clearing receipts may fill omissions from envelope.- Authoritative refreshes required: unknown rows, incomplete ancestor coverage, broad/catalog changes, Gateway-owned filters, failed reads, uncertain owner-prefix boundaries. Overlapping reads recheck later row receipts against their accepted membership; covered facts merge without trailing reads. Events never create filtered membership; only complete unfiltered active-only Current Work windows may admit certified full active rows below.
- Sidebar windows may exclude certified rows below a full recency window; owner-first overflow crossing into the shared page still needs authority. Involving-me members need current viewer ownership or participant receipts and explicit non-hidden state; bounded participants cannot prove non-membership or admit unseen members.
- Compact rows + bounded source attribution. Enrichment flags/global-or-unknown inclusion preserve membership when kind unchanged; dashboard filters need matching
hasBoard/boardFacereceipts. Gallery pagination extends shared managed window; load full settings from descriptors on demand. Child membership: Gateway retention owner'schildOwnerSessionKeys. Parent-only events must certify complete child window; unheld ancestors need explicit exclusion or admitted reference resolved through connection row provenance. - Cached lineage re-adoption changes presentation, not managed membership. Preserve fresh descriptor/event invalidation. Apply admitted descriptor rows immediately; incomplete ancestor coverage gets one paced coordinator refresh, never read per event.
- Unfiltered History/Current Work: apply admitted rows without list refetches. History's shared row-provenance owner retains field clocks, recap updates, clearing receipts; returned lists alone own membership.
excludeSubagentsignores key-proven child exclusions only with complete access-scoped ancestor coverage; held parents/certified references still update. Missing held parents/supplied unheld ancestors need authority; named unheld parents absent from certified coverage may be invisible. Merge live facts on refresh, never replay/coalesce History packets. Missing initial membership needs catch-up; query/connection changes reset provenance. History: forward activity clocks required; preserve omitted recap enrichment; person/search filters retain authoritative refreshes. - Current Work: coalesce only consecutive full active snapshots of one generation, retaining first receipt/latest tail. Partial events, references, terminal snapshots, other identities = FIFO barriers. Complete window below limit may admit certified active row with real session ID/kind, sampled after accepted list + generation retirement/liveness clocks; preserve Gateway cron-run exclusion. Truncated/full windows, conflicting generations, partial unknown rows, ambiguous settlement, incomplete-window removals need authority.
- Current Work: apply ancestor snapshots/references via shared reconciler; pending references retain prerequisite full-row receipts. Controller caps generation-coalesced fence records at 1,000; reconciler owns independent retirement, liveness-observation, generation-authority facts. Only retirement prunes stale returned rows. Fresh certified full rows may resolve older clocked liveness; unclocked observations/conflicting generations need list read. Query/connection changes clear records. Authoritative lists clear uncertainty + covered retirements before pending replay, preserving later retirements. Saturation blocks unseen admission until authoritative read restores bounded state.
- Healthy applied row traffic retains one fallback refresh after at least 60 seconds, including whole-query people/pulse facets.
src/lib/sessions/event-refresh-coordinator.tsowns pacing + pending/trailing invalidation: sample four-to-five-second collection window once when armed; events cannot postpone it. After automatic refresh: wait three times its duration, bounded to five–15 seconds. - Accepted local creates refresh observed list membership independently of primary reads/bootstrap. Create events skip coordinator collection/backoff but retain visibility, bootstrap admission, and queued navigation ownership; the Gateway still owns owner/involvement/search filtering. Other event invalidations retain coordinator pacing.
- Explicit refresh, filter/agent changes, reconnects, foreground replacement bypass backoff + absorb pending invalidation. Recheck visibility/current intent after background admission; hidden pages retain one catch-up until visible.
- Reload cost is main-thread render/reconcile triggered per Gateway message; JSON parse and transport are negligible. Measured no win or regressions (2026-10-15, Team-scale bench, quiet AWS, ≥7 interleaved pairs): socket worker/SharedWorker, delivery scheduling/chunking, batched boot RPC, V8 compile hints/chunk sizing, cached DOM shell replay, WebSocket permessage-deflate (server keeps it off). Retry only with new evidence.
- Cut work at its owner: memoize values read per item (
createMemo), render reused UI as components rather than helper calls, keep row identity through placement/history/live updates, check cheap state before reading layout. - Measure with
scripts/bench-control-ui-cold-load.ts(long-task and render-pass probes) over several untraced runs; single DevTools traces inflate timing.
- Foreign
src/i18n/locales/*.ts= stable source-owned lazy adapters; translations from canonical groupedsrc/i18n/.i18n/*.tm.jsonl. Never hand-edit translation memory, locale/fallback metadata unless targeted generated-output fix requested. - English:
src/i18n/locales/en.ts, staticen-agents.ts, lazyen-*.tsregistrars.scripts/lib/control-ui-i18n-catalog.tsowns ordered composition + raw source hashes for generation, verification, Vite; reads.catalogdata without runtime registration. Wiring owners:scripts/control-ui-i18n.ts,scripts/lib/control-ui-i18n-sync-plan.ts,ui/config/control-ui-locales.ts,ui/src/i18n/lib/{types,registry}.ts. - Register lazy English synchronously at each consumer, including Settings search before page load. Keep startup/shared copy eager; preserve shared
en+ sibling namespaces. Retain empty subtree anchors when extraction changes flattened order/grouped memory aliases. Never import host catalog owner into runtime. - Source PRs: update English/adapters/wiring, run keyless
pnpm ui:i18n:baseline, commit changed raw-copy baseline. Exclude fallback/locale metadata + translation memory: CI rejects mixed diffs except canonicalrelease/YYYY.M.PATCHbranches or detected complete canonical-memory migrations. - Deterministic keyless
pnpm ui:i18n:verify: English shape, runtime wiring, raw-copy drift in lint/changed-check UI. Foreign parity: post-merge bot + strict generated-output gate. - Serialized
control-ui-locale-refresh: translate after merge, open generated PR with exact-head auto-merge. Authenticatedpnpm ui:i18n:sync: maintainer/release repair path; new keys require provider auth. Drift: workflow/release prep, never manual translation/merging generated outputs. pnpm release:prepsyncs before freeze;pnpm ui:i18n:check= zero-fallback release/generated-output gate.pnpm ui:i18n:report= hardcoded-copy priorities + fallback metadata, not drift.
pnpm lint: Solid 2 oxlint rules onui/**/*.tsxand TypeScript helpers insrc/lib/reactive/,src/components/solid/, andsrc/test-helpers/; plain domain owners keep their own contracts. Follow the Solid idioms for split effects, tracking, and lifecycle.tools/solid-lintisolates plugin tooling dependencies; corpus:test/scripts/oxlint-solid.test.ts.pnpm lint:ui:styles(included in lint):ui/srcstylesheets + Lit CSS templates viaconfig/stylelint.config.mjs/postcss-lit. Stylelint owns error rules; oxfmt owns formatting.- Shared 24×24 Lucide icons:
strokeIcon()insrc/components/icons-tools.tsfor inline stroke attributes inside shadow roots. Bodies must besvgtemplate fragments, neverhtml(wrong namespace). pnpm lint:ui:lit= slow opt-in template diagnostic with existing findings, not CI gate.
- No universal targets/pseudo-elements after
:has()compound,:has()with::placeholder, or descendants after sibling-relative:has(+ …)on repeated items. Use owner-set classes, named children, or subject custom property. No:has()subjects on.shell,.content,.chat-thread,.chat-split-view,:root,html,body, including modifiers,:is()/:where(), nested&: insertions restyle subtree; stylelint cannot resolve nesting. Third-party global CSS uses Vite PostCSS;config/control-ui-web-awesome-page-rule.tsdrops Web Awesome's never-matching:is(html, body):has(wa-page)rule. - New-tab links/controls: pointer cursors; state-changing controls: default arrow.
- Colors:
src/styles/base.csstokens (color-no-hex). Stated exemptions: token definitions,lobster-pet.cssartwork,--theme-chip-*previews. Prefer tokens in currently ungated Lit CSS templates too. max-widthbreakpoints: 400/560/640/768/900/1100/1320px + 932×500 landscape-phone compound. Round new thresholds up; new rungs require config comment + this rule updates.- Duplicate selectors = errors; deliberate topic reopens need
stylelint-disable-next-line no-duplicate-selectors -- <reason>. node --import tsx scripts/audit-control-ui-dead-css.mtsadvisory. Verify selectors before deleting; extend dynamic-stem detection, not allowlist.- Nest CSS only while rewriting section; no conversion sweeps.
- No
@layer: shared light-DOM CSS depends on import order/specificity + lazy page CSS. Reconsider only with computed-style parity across all routes on mocked dev server.
- Place workflow completion actions last in
<openclaw-modal-actions>, after content, status and validation errors. The native modal stylesheet owns right alignment, wrapping and label containment for Solid and retained Lit callers. Keep submit buttons inside their native form; no renderer or custom-element registration is needed. Caller styles own spacing, not competing alignment or mobile full-width rules. Header X/Expand and field-local controls are not completion actions. Scrollable panels respect--openclaw-modal-height-limitso the footer stays reachable. Set the intended width with--openclaw-modal-widthon the native host and let child panels fill the shell instead of imposing a wider independent width.
- UI/Gateway ship as one install/version. Never older-Gateway method fallbacks/version-conditional UI behavior.
isGatewayMethodAdvertisedonly gates config/plugin-dependent features. - Handshake rejects Gateway-served same-origin skew. Exempt
pnpm ui:dev, customgateway.controlUi.root, cross-origin/connection-settings dialing remain unsupported for mismatch: no compat, visible failure at first missing method. Rejection at connect = separate server-owned product change.
- Generated
config/control-ui-boot-modules.jsonrecords ready/new+/chatcaptures: fetched modules reachable from HTML/requested dynamic entries, static imports, dynamic imports resolving into fetched chunks. Keep route-exclusive/sharedcontrol-ui-boot-*groups separate inconfig/control-ui-chunking.ts; co-located chat-only code must stay out of New Session.entrieslists requested dynamic entries;config/control-ui-boot-preloads.tsfollows static dependencies into inert route preload templates activated by Gateway. CSS hints preload bytes without changing insertion order. - Material boot-path changes:
pnpm ui:boot-manifest:gen, thenpnpm ui:buildto verify grouped output. Generation: temporary build with measured groups disabled/inactive templates, preventing stale-capture feedback. Never hand-edit manifest.
- Gateway serves
dist/control-ui; source edits needpnpm ui:build. Before trusting live result: verify served/assets/index-*.jshash changed.
- UI-specific rules here; root
AGENTS.mdowns repo-global workflow.
- Substantial UI design changes: follow Control UI stress test; inspect states in HTML gallery + collect per-example feedback.
- Never capture Discord invitation card. Mock/E2E harnesses seed canonical browser dismissal; invitation tests may use
communityInviteDismissed: falsewithout screenshots/videos.