Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
92321fc
docs: add refactoring guide, audit and phased plan
Linaryx Sep 28, 2026
1eba59a
fix(chat): keep badge visibility reactive to config changes
Linaryx Sep 28, 2026
4484e2f
refactor(icons): consolidate ui icons onto a typed package
Linaryx Sep 28, 2026
a628860
docs: record phase 3 outcome in the refactor plan
Linaryx Sep 28, 2026
e134f83
refactor(styles): remove dead legacy tokens and resolve the border co…
Linaryx Sep 28, 2026
17967ce
chore(chat): remove superseded and unreferenced modules
Linaryx Sep 28, 2026
fd31334
docs: record phase 2 and phase 7 progress in the refactor plan
Linaryx Sep 28, 2026
19f2d9d
refactor(setup): extract chip input and colocate feature components
Linaryx Sep 28, 2026
018174a
refactor(chat): remove duplicated render helpers
Linaryx Sep 28, 2026
2e4af5b
refactor(setup): extract persistence adapter
Linaryx Sep 28, 2026
f10af40
refactor(setup): extract bot profile lookup
Linaryx Sep 28, 2026
bc618af
refactor(setup): extract local font capability
Linaryx Sep 28, 2026
1040b15
refactor(setup): extract form value coercion
Linaryx Sep 28, 2026
2779a89
refactor(setup): extract overlay url projection
Linaryx Sep 28, 2026
83b24b7
refactor(setup): extract preview synchronizer
Linaryx Sep 28, 2026
4a73f61
refactor(setup): extract document lifecycle helpers
Linaryx Sep 28, 2026
01e5d8d
refactor(setup): extract settings search
Linaryx Sep 28, 2026
b8afa9e
refactor(setup): extract user chip and ffz merge block
Linaryx Sep 28, 2026
01c094b
refactor(chat): give singleton caches and generated styles a teardown…
Linaryx Sep 28, 2026
b96c637
refactor(chat): merge entry animation cleanup and colocate badge prio…
Linaryx Sep 28, 2026
9cdb2a6
refactor(styles): publish overlay presets as custom properties
Linaryx Sep 28, 2026
03f563f
refactor(chat): split animation vocabulary from style generation
Linaryx Sep 28, 2026
8c56ca3
refactor(chat): inject url resolution into the message renderer
Linaryx Sep 28, 2026
8b096a2
refactor(setup): extract section descriptors
Linaryx Sep 28, 2026
c652d5c
refactor(setup): extract preview controls with their thumb measurement
Linaryx Sep 28, 2026
9291b8a
chore(build): drop the removed icon package from the ui chunk group
Linaryx Sep 28, 2026
f7e1ec7
fix(chat): release the animation stylesheet with the overlay styles
Linaryx Sep 28, 2026
1c8965d
docs: record the closing verification for the refactor plan
Linaryx Sep 28, 2026
cd17aae
refactor(icons): render through the official hugeicons solid package
Linaryx Sep 28, 2026
3baf16a
docs(agents): publish the engineering rules at the repository root
Linaryx Sep 28, 2026
5121bf4
refactor(architecture): remove the setup layer inversions
Linaryx Sep 28, 2026
a7f6fce
fix(chat): make application teardown idempotent and terminal
Linaryx Sep 28, 2026
bfd8102
fix(chat): stop stale asset loads from repopulating reset stores
Linaryx Sep 28, 2026
96200bd
fix(chat): let the preview runtime release the document state it claims
Linaryx Sep 28, 2026
0890919
refactor(setup): make the document lock own state instead of a colour
Linaryx Sep 28, 2026
1f5c508
refactor(chat): move the layout and fade runtimes beside their owner
Linaryx Sep 28, 2026
27b6096
fix(chat): release the generated paint stylesheet with its runtime
Linaryx Sep 28, 2026
feb4e11
fix(setup): dispose the preview synchronizer on route cleanup
Linaryx Sep 28, 2026
adabb32
test(chat): cover re-applying the overlay styles after cleanup
Linaryx Sep 28, 2026
58a017b
docs(debug): drop the stale todo references in the perf panel
Linaryx Sep 28, 2026
119e083
docs(refactor): reconcile the architecture and close the follow-up
Linaryx Sep 28, 2026
36b2206
fix(chat): release the last two runtime resources at teardown
Linaryx Sep 28, 2026
0ef2d54
docs(refactor): record the review findings in the completion report
Linaryx Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,157 @@
# ChatYX Agent Instructions

These rules apply to every coding agent working in this repository. They are the
short form of the engineering contract; `documents/ARCHITECTURE.md` and
`documents/DESIGN.md` hold the detail.

## Required reading

Before non-trivial work, read what the change touches:

- `documents/ARCHITECTURE.md` — layers, ownership, the setup feature, icons, tokens
- `documents/DESIGN.md` — the token contract and the overlay's visual model
- relevant tests for the area being changed

Repository-wide refactors additionally require:

- `documents/REFACTORING.md` — the full engineering rules and refactoring workflow
- `documents/REFACTORING_FOLLOWUP.md` — the current completion supplement

Do not start a repository-wide cleanup after reading one or two files.

## Architecture direction

```text
routes -> features -> services/config/utils
-> platform adapters
```

- `routes/` bind Solid signals and JSX to a feature's public API. A route owns
visibility and composition, not infrastructure, network clients or timers.
- `features/*/application` coordinate use cases and own lifecycle.
- `features/*/model` hold pure state transformations and calculations.
- `services/` implement network, browser, provider, storage and platform
capabilities.
- `config/` holds schemas, normalization, parsing, serialization and defaults.
- `utils/` holds small genuinely reusable helpers, never domain logic.

A service must not import a feature, a route or a component. A feature must not
import a component: shared contracts live in `features/*/model`, `config/` or the
`services/` layer, and components import them, never the reverse. The one
recorded exception is that a `config/` module may depend on a dependency-free
`services/` leaf (`config/setupTemplates` on `services/storage/setupStorage`).

Do not add a service locator or a DI container. Pass dependencies through
constructors, factories or parameters.

## Composition roots and lifecycle

Every long-lived resource — socket, timer, interval, subscription, observer,
listener, mutable cache, reconnect loop, runtime — has exactly one owner, and
that owner releases it.

- `createChatOverlayApplication` is the chat route's composition root: one live
or preview runtime plus one predictions controller.
- Construction must not create an owned resource that `destroy()` refuses to
release. `destroy()` is safe before `start()`, safe when called twice, and
safe while `start()` is still awaiting.
- Teardown is an ownership boundary, not a reset call: asynchronous work started
under a runtime must not repopulate shared state after that runtime is
destroyed. Singleton services guard their commits with a generation they
capture when the work starts.
- Preview and live runtime own different things in the same document. Reset a
process-wide singleton only from the owner that installed it.
- Browser-global events are for real browser integration only, never as an
internal event bus.

## UI primitives

`src/components/ui/` holds generic primitives with no ChatYX domain knowledge —
no Twitch, YouTube, Kick, OBS, TTS, RTE, setup-section or chat-config concepts.

```text
components/ui generic primitive
components/<feature> feature presentation
routes/* page composition
```

Check for an existing primitive before writing a control. Do not push a bespoke
control through a generic primitive when that changes its design; extract only
genuine duplication, and put feature wrappers in `components/<feature>/`.

## Icons

- Generic UI icons: `@hugeicons/core-free-icons`, rendered through
`src/components/ui/icon.tsx`, which is a thin wrapper over the vendor's
`HugeiconsIcon` from `@hugeicons/solid-js`. The wrapper owns `currentColor`,
the `1em` default size and decorative-by-default accessibility; the vendor
owns SVG rendering; the icon payload owns its stroke width.
- Import icons by subpath (`@hugeicons/core-free-icons/TextIcon`). Never
`import * as Icons`, never a runtime string lookup, never an icon font.
- Brand glyphs (Twitch, YouTube, Kick, GitHub, 7TV) have one owner:
`src/components/brand/PlatformGlyph.tsx`. Do not replace them with generic
icons and do not duplicate them as a public SVG.
- Bespoke graphics — sparklines, event, reply and role artwork, custom
visualizations — stay custom. Consistency means one system per
responsibility, not one renderer for every drawing.
- Decorative icons stay hidden from assistive technology; icon-only controls
carry an accessible name.

## Styling

Tailwind, CVA, `cn()` and Kobalte are the current stack. Do not add another CSS
framework, and do not remove Tailwind to prepare for a future one.

- Static CSS owns rule shape. The runtime publishes values as CSS custom
properties: `getOverlayStyleVariables()` → `OverlayStyleManager` → document
root → `chat.css`. Do not go back to one generated stylesheet per preset.
- Generated CSS is valid only where the rule shape is provider data: 7TV paint
and emote modifiers. Ask whether stable CSS plus a custom property expresses
the behaviour before generating CSS.
- A reusable design decision becomes a token; a value that changes while the
overlay runs stays a custom property. Do not turn every literal into a token.
- Inline styles are for genuinely dynamic values, not for static design.
- Classify `!important` before touching it. Keep the OBS transparency override
and provider/emote overrides; fix specificity problems in CSS instead.
- Do not migrate overlay, provider or runtime-generated markup to Tailwind.
- Lifecycle modules control state and ownership, not visual design values. Put a
product colour in CSS, not in a document helper.

## Accessibility, OBS and public contracts

- Refactors preserve labels, ARIA relationships, keyboard navigation, focus
visibility, semantic buttons, disabled state and screen-reader text.
- The overlay is an OBS browser source: transparent page and container
background, stable source order, wrapping contracts, emote modifiers,
provider cosmetics and the user's font/colour/stroke/shadow choices.
- Public contracts are frozen unless the task is explicitly a breaking change:
chat URL parameter names and defaults, overlay URL shape, the preview message
protocol, setup import/export meaning, platform source configuration, and the
stored setup keys (`chatyx.setup.*`). `buildOverlayUrl` is the single producer
of the overlay link; `tests/setupConfig.test.ts` and `tests/formValues.test.ts`
pin the round trip.
- No visual redesign during structural work.

## Dependencies

One clear responsibility per library. Before adding a dependency, answer what it
owns, whether that is already covered, and what it costs. Remove an old library
only after every real usage is migrated and the build passes.

## Tests

Prefer characterization tests at the layer that owns the behaviour: injected
factories, deferred promises, minimal `window`/`document` shims, pure projection
tests, service-level tests. Do not add a component-render environment for its
own sake, and do not snapshot generated class names. Never delete a valid test
to make a refactor easier, and never claim green without running the commands.

Full validation before declaring work done:

```bash
bun run check # lint + typecheck + tests + build
```

## Commit Messages

Use [Conventional Commits](https://www.conventionalcommits.org/) for every commit.
Expand All @@ -10,4 +162,5 @@ Follow the project rules in [CONTRIBUTING.md](./CONTRIBUTING.md) and the
- Use an imperative, lower-case description without a trailing period.
- Use a logical project area as the scope; do not use issue IDs as scopes.
- Use `!` and a `BREAKING CHANGE:` footer for breaking changes.
- One concern per commit. Do not combine unrelated migrations.
- Run `bun run check` before committing when practical.
9 changes: 6 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 112 additions & 2 deletions documents/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@ routes -> features -> services/config/utils

- `routes` bind Solid signals and JSX to a feature's public API.
- `features/*/application` coordinate use cases and own lifecycle.
- `features/*/model` contains pure state transformations and view calculations.
- `features/*/model` contains pure state transformations and contracts shared
with other layers.
- `services` implement platform and browser capabilities.
- A service must not import a feature or route.
- A feature must not import a component. A contract that both a feature and a
component need lives in the feature's `model`, in `config/` or in `services/`,
and the component imports it — never the reverse. `SetupSectionId` is the
worked example: it lives in `features/setup/model/setupSections.ts` because the
route, the workspace layout and the settings search all need it.

Feature internals use direct imports. A feature-level `index.ts` is a public API for consumers
outside that feature.

## Chat overlay
## Chat overlay lifecycle and asset ownership

`createChatOverlayApplication` is the composition root for the chat route. It constructs exactly
one live or preview runtime and one predictions controller.
Expand All @@ -31,6 +37,30 @@ channel.tsx
-> PredictionController
```

Construction and teardown agree about ownership: `ChatOverlayApplication.destroy()`
releases what its constructor built, is idempotent, and is terminal, so it is
also safe while `start()` is still awaiting `initialize()`. A runtime that is
destroyed stops publishing: the live runtime guards every step of its
initialization with a generation, and the preview runtime ignores configuration
and timer callbacks after destruction.

Asset state has one owner per document. `emoteService`, `badgeService`,
`sevenTVCosmeticsService`, `mentionStyleService` and `chatFeatureIntegration` are
process-wide singletons, and the runtime that filled them is the runtime that
resets them in `destroy()`. Resetting alone is not enough, because a request
that is already in flight can still resolve: each of those services carries a
generation that `reset()` (or `clearAllCaches()`) bumps, and a load captures it
on entry so a late response cannot commit into the store the next runtime owns.

`ChatAssetLoader` needs none of that: it is constructed per live runtime, and its
shared-channel map and channel-scoped caches die with the instance.

The preview runtime is the only runtime in its document — the setup page loads it
in an iframe, which has its own module instances — so its teardown clears the
process-wide state it claimed. For the same reason the RTE proxy flag is a plain
boolean rather than a lease or a reference count: exactly one chat runtime exists
per document, and only the runtime that set the flag clears it.

Each runtime owns the resources it starts and releases them in `destroy()`. Cross-module events
use typed callbacks; browser-global custom events are reserved for actual browser integration
boundaries, not communication between services in the same object graph.
Expand All @@ -52,9 +82,89 @@ src/services/chat/
twitch/ # IRC, GQL, and recent-message clients
```

`runtime/` also owns the layout and fade managers. They are chat overlay
infrastructure, not general helpers — the stylesheets they publish are written
against `#chat_container`, `.chat_line` and `.message-fade-out` — so they sit
next to the pipeline that owns them instead of in `utils/`.

Provider folders do not expose internal barrels. Cross-feature consumers use
`src/services/chat/index.ts`; chat internals import concrete sibling modules directly.

## Setup feature

`src/routes/setup.tsx` is a UI composition root: it binds signals, wires feature
APIs into effects, composes sections and renders JSX, and nothing else.
Everything with a contract of its own lives outside it.

```text
src/features/setup/model/setupSections.ts # setup section identity
src/features/setup/previewSync.ts # the setup side of the preview iframe protocol
src/features/setup/settingsSearch.ts # searchable elements, highlights, counter, reveal
src/features/setup/setupDocument.ts # document scroll lock and reduced-motion watch
src/config/setupConfig.ts # form state -> ChatConfig, ChatConfig -> overlay URL
src/config/formValues.ts # form string -> config value, mirroring chatUrlParams kinds
src/config/setupTemplates.ts # built-in and user templates, validation, serialization
src/services/setup/botProfiles.ts # Twitch/Kick bot lookups (profiles)
src/services/setup/channelSearch.ts # Twitch/Kick channel search and its normalizers
src/services/setup/logins.ts # pasted-login normalization shared by the three lists
src/services/setup/localFonts.ts # Local Font Access capability
src/services/storage/setupStorage.ts # every localStorage access the setup page makes
src/components/setup/* # section layout, rows, chips and the feature's controls
src/components/setup/setupText.ts # label resolution, kept free of JSX so it stays testable
```

The row descriptors under `components/setup/sections/` are pure descriptor
factories: they take one source object of signals and callbacks and return the
rows. They import translations, UI primitives and types only, so they cannot
grow into a second route.

The overlay URL is a public contract: deployed OBS browser sources depend on it.
It is therefore produced in one place, `buildOverlayUrl`, and covered by tests
that round-trip through `parseChatConfigFromSearchParams`. Renaming or dropping a
query parameter is a breaking change, not a rename.

`config` may depend on a dependency-free `services` leaf (as `setupTemplates`
does on `setupStorage`), because services are the lower layer for capabilities.
The reverse — a service importing a route or a feature — remains forbidden.

## Icons and brand assets

UI icons come from `@hugeicons/core-free-icons` and are rendered through
`src/components/ui/icon.tsx`, which is a thin wrapper over the vendor's
`HugeiconsIcon` from `@hugeicons/solid-js`. The vendor owns SVG rendering; the
wrapper owns only the project convention — `currentColor`, `1em` as the default
size, and decorative-by-default accessibility — and the icon payload keeps its
own stroke width.

There is no icon font, no `hgi-*` class string, and no inline SVG standing in for
a UI icon. Icons are imported by subpath (`@hugeicons/core-free-icons/TextIcon`)
because the package barrel is 675 KB while a single icon is under a kilobyte.

Two kinds of drawing are deliberately not UI icons and stay inline: monochrome
platform glyphs, which have exactly one owner in
`src/components/brand/PlatformGlyph.tsx`, and bespoke visuals such as the
sparkline and the event, reply and role graphics. The public brand chips under
`public/img/` are a different rendering for a different surface, not a duplicate
of the glyph components.

## Plain and generated CSS

Design tokens are consumed as CSS custom properties. `src/styles/chat.css` owns
the overlay's rules and `overlayStyleManager` writes only property values and two
root attributes, so the rule shape is fixed at build time.

Two boundaries are still allowed to generate CSS, because there the rule shape is
data supplied from outside rather than a design decision:

- provider cosmetics (7TV paint), where colours and gradients arrive as protocol
data;
- emote modifiers, where geometry depends on the emote's own dimensions.

Everything else prefers a stable rule plus a custom property. The same split
applies away from the overlay: the setup page's document lock is a `data-*`
attribute on the root and `SetupWorkspace.css` owns what that means, so the
lifecycle helper holds state rather than a colour.

## Refactoring rules

1. Extract ownership before extracting files: every timer, socket, listener, and mutable cache has
Expand Down
Loading
Loading