| name | develop-frontend |
| description | Implement or review Synergy Web and shared UI changes across packages/app and packages/ui. Use for components, contexts/stores, navigation, settings, dialogs, workbench surfaces, semantic icons, themes, responsive behavior, accessibility, frontend API calls, event sync, and product interaction changes. |
Develop the Frontend
Read the Contracts
- Read
packages/app/AGENTS.md and Web product contract.
- Read Frontend data sync for contexts, snapshots, events, streaming, composer intent, or loaded buckets.
- Read Browser runtime for Browser UI or Desktop/Web presentation changes.
- Load
change-server-api when the UI needs a new or changed server contract; load add-tool for tool-card presentation.
Preserve State and API Ownership
- Use stores for coherent keyed collections and signals for independent scalar state.
- Apply entity updates with targeted setters and
reconcile; do not replace a whole stored object for a one-field event.
- Keep derived values one-way. Preserve composer resolution as explicit draft → session default → fallback; only explicit user choices persist upward.
- Use generated SDK methods for ordinary internal HTTP routes. Keep raw browser transports only for WebSocket/EventSource/WebRTC, external URLs, platform fetch injection, and browser file/blob/download flows that the SDK should not represent.
- Preserve Scope/directory parameters, authentication, error semantics, asset URLs, event
seq/epoch, replay, and loading/error states.
- For append-only LLM streams, keep the full snapshot as recovery state while imperative renderers track an offset and consume only the appended suffix through the dependency's typed live-update API. Do not rescan the accumulated prefix or insert an independent character-rate playback backlog; reset from a checkpoint only when the append invariant breaks. Derive terminal presentation from explicit part or message lifecycle markers, not coarse session status or the presence of a later timeline part. Key imperative terminal transitions and enhancements by content identity so unrelated sibling updates cannot restart them.
- Do not key the whole turn or message-role boundary by object identity:
message.updated events land as fresh objects on every update, so a keyed Show/Match destroys and rebuilds the entire turn (tool-card expansion, scroll, and Markdown state included) several times per reply. Keep those boundaries non-keyed responsive updates, and instead isolate failures inside Part with an identity snapshot: capture session/message/part/type before child evaluation, render the ErrorBoundary fallback from that snapshot only, and never re-read the failed child's live props. Log the original error with stable IDs only; exclude message content, tool payloads, and workspace paths. Cover this with a real Solid lifecycle test that proves a secondary stale read cannot escape the local boundary.
- Read session-shaped store fields through the session data view only: ui components use
useData().view (partsFor/messagesFor/permissionsFor/statusFor/inboxFor/hasInboxBucket/todosFor/dagNodesFor/questionsFor/cortexTasks/sessions/sessionFor), app components use useSessionDataView(). Do not read data.store.part/permission/... or sync.data.<field>[sessionID] directly in render code — session switches race store intermediate states (missing buckets, released scope stores) and createMemo defaults stop applying after first compute, so a direct read can surface undefined and crash with a rotating set of TypeErrors. The view accessors apply their empty fallback inside the function body. Missing array buckets must resolve to the shared constants in packages/ui/src/context/session-data-view.ts (EMPTY_PARTS, EMPTY_MESSAGES, …) — never a fresh array literal, because the render chain's same() equality guards short-circuit on reference identity and a fresh literal would invalidate every downstream memo on each store tick. hasInboxBucket is the only accessor that reports bucket presence: use it to preserve an "not loaded yet" gate when undefined semantics matter (e.g. inbox loading state).
- Callback parameters are accessors or raw values depending on the control: non-keyed
<Show> and <Index> pass an accessor — call it (param()) before rendering or passing it into i18n values, formatters, or attributes — while <Show keyed> and <For> pass the raw value itself. Passing an accessor uncalled renders its minified source (the crash-page footer once displayed Version: () => { if (!untrack(condition)) … }), and calling a keyed <Show> or For parameter fails typechecking. Prefer the non-callback form reading the source signal directly when no narrowing is needed.
Preserve Browser Capability Boundaries
- Route ordinary App/UI identifiers through
generateUUID() or generateRandomBytes() from the shared utility package. Do not call crypto.randomUUID() or crypto.getRandomValues() directly from browser source.
- Use
generateSecureUUID() or generateSecureRandomBytes() for authentication state, nonces, credentials, and other security-sensitive values. A missing secure source must fail only the affected operation; it must never fall back to Math.random().
- Keep optional browser APIs out of module-scope startup paths. Gate Clipboard, Notifications, credentials, media, and other Secure Context capabilities at the owning action and provide a local unavailable or error state.
- Treat non-loopback private-network HTTP as a supported Web deployment. When capability or bootstrap code changes, verify an actual non-Secure Context rather than relying on localhost.
Localize Product UI
Read Frontend localization before adding product copy, accessibility text, locale-sensitive formatting, or language settings.
- Use the App-owned Lingui runtime and explicit semantic message IDs in the form
{domain}.{component}.{semanticKey}. Use runtime descriptors or <Trans>; do not use Lingui macros, dynamic IDs, language branches, module-load translation calls, or sentence fragments assembled in code.
- Keep descriptors statically extractable with an English default message and a translator comment when product context is not obvious. Use ICU variables, plural/select syntax, and component placeholders for complete messages.
- Translate Synergy-owned chrome, actions, states, recovery guidance, and accessibility labels together. Keep user, LLM, Note, source-code, terminal, browser-page, plugin-author, brand, path, identifier, and raw diagnostic content verbatim.
- Use the shared active-locale formatter for dates, time, numbers, percentages, currency, lists, and relative time. Do not hard-code locale tags or use a regional locale to imply an unrelated preference such as 24-hour time.
packages/app owns locale state, catalog loading, Settings, persistence, bootstrap mirror reconciliation, and the global I18nProvider. packages/ui consumes that provider through peer dependencies; it does not create a second runtime, import App contexts, inspect browser locale, or own catalogs.
- Keep the Settings language control global, responsive, and recoverable: Follow System, English, and Simplified Chinese apply without refresh, do not follow project Scope, and must preserve language self-names so a user can switch back after a mistake.
- Run extraction after each coherent copy change, translate every new
zh-CN message, remove obsolete entries, and keep strict compilation green. Finish with the repository localization contract so new hard-coded product text cannot bypass the catalog.
Use Semantic Icons
Non-tool product UI expresses meaning through packages/ui/src/components/semantic-icon.tsx.
- Name the user-facing meaning before choosing a glyph.
- Reuse an existing token only when the new control has the same meaning. Similar appearance or location is not enough.
- Add a new token to
packages/ui/src/components/semantic-icon.tsx before using an icon for a new product entity, navigation concept, state, setting, command, or action.
- Choose a built-in glyph that is not already mapped to another semantic token. Reuse the existing token when the meaning is truly identical; do not create a second token that aliases its glyph.
- When the glyph is new to the shared Icon component, register it in both
packages/ui/src/components/icon.tsx and packages/ui/src/plugin/builtin-icons.ts before referencing it from the semantic map.
- Render through
getSemanticIcon(token) and type stored metadata as SemanticIconTokenName.
- Keep raw icon names inside base icon controls, file-type/icon registries, tool-card plumbing, or plugin-provided icon paths. Built-in Plugin host UI still uses semantic tokens. Tool icons follow
add-tool, not the product semantic-token registry.
- Route Composer completion, annotation, revision-checked edits, and normal-message preflight through the single
ComposerDocumentController; do not let features or plugin adapters read and mutate contenteditable independently. Keep Composer and selected-text snapshots transient and out of sync/replay stores.
Run bun test test/semantic-icon.test.ts from packages/ui. It rejects duplicate glyph mappings, missing shared registrations, raw JSX icon literals, and raw icon object metadata outside the documented base/tool/plugin-data exceptions.
Preserve Product Presentation
- Reuse shared workbench, dialog, form, toolbar, and surface primitives before creating local variants.
- Preserve polarity: dark content/selection surfaces step brighter inward; light surfaces step darker inward.
- Use semantic color/type/spacing tokens. Reserve state colors for real state rather than decoration.
- Keep controls labeled, keyboard reachable, focus-visible, WCAG AA, reduced-motion safe, and usable at narrow widths.
- Implement loading, empty, error, disabled, and reconnect states as first-class behavior.
- Update
PRODUCT.md when an interaction or visual rule should survive refactors.
- For imperative renderers, use the dependency's typed live-update API and cover it with a boundary test. Do not hide an unsupported method behind a cast; same-mode theme changes must repaint already-mounted renderers.
Preserve Loading Boundaries
- Register optional built-in workbench panels with
WorkbenchPanelEntry.loader; do not statically import Notes, Files, Browser, Terminal, or Review implementations into the route shell.
- Keep heavyweight feature engines behind the interaction that needs them: Tiptap and Mermaid behind Notes, Monaco behind file Source view, and Ghostty behind Terminal.
- Import only fonts used by the active product typography contract. A dormant family must not be emitted by the default App build.
- Preserve
packages/app/test/app-build-css-contract.test.ts as the production build regression gate for initial module preloads, emitted product fonts, and core compiled CSS.
Change Themes and Color Tokens
Read docs/reference/frontend-theming.md before changing the color contract, adding a semantic token, integrating an imperative renderer, or authoring a selectable theme.
- Use
packages/ui/src/theme/tokens.ts as the exhaustive color-token catalog and resolve.ts as the only palette resolver. A theme supplies light/dark seeds plus optional typed overrides; do not create a parallel CSS palette.
- Use a canonical token in Tailwind utilities and CSS variables. If the required meaning is absent, add it to the token catalog and resolver before using it. Do not invent consumer aliases such as
surface-*-soft, surface-muted, or unregistered status text names.
- Edit
packages/ui/src/theme/themes/synergy.json for Synergy-specific seed or override values. Run bun run --cwd packages/ui generate:theme; never hand-edit theme.generated.css, tailwind/colors.css, or theme.schema.json.
- Keep common text/background and status foreground/surface pairs at WCAG AA contrast in both modes. Preserve the product polarity rule independently of accent hue.
- Plugin themes are complete structured JSON themes validated by the same schema and resolved by the same runtime. Do not add arbitrary plugin CSS theme overrides or theme-only token paths.
- Imperative consumers such as Canvas, Monaco, terminals, and embedded documents must use the resolved theme tokens and react to the canonical theme-change event. Do not maintain component-local light/dark palettes or infer a theme change only from
data-color-scheme.
- Run the theme contract, artifact parity, and consumer-utility tests before visual inspection:
bun test --cwd packages/ui test/theme.test.ts test/theme-generation.test.ts
bun test --cwd packages/app test/testing/color-token-contract.test.ts
Verify
- Run the narrow component, model, or context test first.
- Run:
bun run --cwd packages/app test
bun run --cwd packages/app typecheck
bun run --cwd packages/ui test
bun run --cwd packages/app build
For browser capability or bootstrap changes, also run:
bun test --cwd packages/app test/testing/browser-crypto-contract.test.ts
bun packages/app/script/private-http-smoke.ts
For localized UI changes, also run:
bun run --cwd packages/app i18n:extract
bun run localization:check
- Inspect both themes, keyboard/focus, narrow layout, and loading/error behavior in an existing app or isolated second runtime.
- At 375 px, check that overlay surfaces are named and keyboard-contained and that every interactive control remains inside the viewport. Open each changed lazy panel once to prove its implementation and resources still load.
- Exercise Desktop when native Browser, window chrome, protocol, or Electron behavior changed.
- Finish with
bun run quality:quick when the change is ready for repository review.
Handoff
Report state ownership, API path, semantic icon token, shared primitives, accessibility states, tests, visual checks, and any durable PRODUCT.md or Skill update.