mainis the only maintained source branch. Make code changes there.mv3,mv3-ltl, andmv2are retired. Do not use them for new work.- There is no branch ladder any more. MV2 and MV3 are build targets on
main, not branches — see "Manifest Version Targets" below. - If a task touches both HyperChat and LiveTL, HyperChat still goes first.
- Cross-repo order is mandatory:
- HyperChat
main - LiveTL
develop - LiveTL
mv3-fr - LiveTL
release
- HyperChat
- Never start cross-repo work in LiveTL when the HyperChat submodule also needs to change.
- If the task also requires syncing YtcFilter (YTCF), do it after HyperChat
mainis updated:- merge HyperChat
maininto YTCFmaster - keep YTCF release notes and its in-product changelog in the strict one-line, lowercase, user-facing style documented in YTCF's
AGENTS.md
- merge HyperChat
- Commit messages should be short, direct, and readable in
git log --oneline. - Prefer active voice and concrete verbs:
hide @ in namesfix lingering yt visualsorder matters
- Avoid padded scopes, issue-number prefixes, and changelog-style essays in commit subjects.
- A slightly dry or funny commit is fine if it is still clear at a glance.
mainbuilds three targets:chrome(MV3),firefox(MV3), andmv2(MV2, Firefox-only).- The
mv2target is not legacy cruft. Firefox's MV3 support is unreliable for LiveTL's needs, so LiveTL's Firefox variant consumes it. Do not "simplify" it away. - Keep the MV2/MV3 distinction in exactly two places:
src/manifest.json—{{mv2}}./{{mv3}}.key prefixes, resolved byscripts/resolve-manifest.ts. The MV tag must come first in nested keys ({{mv3}}.{{firefox}}.background); the reverse order leaks a literal tag into the built manifest.__MV__in source — a build-time constant, so unused branches are dropped per target.
- Do not add per-MV source files (
chat-background.mv2.tsand friends).main's architecture — thin background plus the broker insrc/ts/messaging.ts— runs under MV2 as-is. Port MV3 patterns down to MV2, never MV2's persistent-background design up. - Prefer shared, untagged config. Only tag a key when MV2 and MV3 genuinely differ.
- Only the two MV3 zips are released.
mv2is built in CI for breakage coverage and consumed by LiveTL via submodule.
src/ depends on three bare globals, declared in src/ts/typings/vite-env.d.ts
and supplied by vite.config.ts for our own builds:
| Constant | Emitted literal | Meaning |
|---|---|---|
__BROWSER__ |
string — "chrome" / "firefox" |
target browser |
__VERSION__ |
string — "3.3.0" |
version written into the manifest |
__MV__ |
number — 2 / 3 |
target manifest version |
__MV__ is compared with strict equality (__MV__ === 2), so it must emit a
number literal. Defining it as the string "2" makes every check silently
false and the MV2 build takes MV3 code paths — no error, just wrong behavior.
Both bundlers do textual substitution, so the value is source text, not a JS
value. JSON.stringify() is the safe spelling in both:
// vite
define: { __BROWSER__: JSON.stringify(browser), __MV__: JSON.stringify(2) }
// webpack — values are code fragments, so a bare string is an identifier:
// __BROWSER__: 'firefox' -> emits `firefox` -> ReferenceError
new webpack.DefinePlugin({ __BROWSER__: JSON.stringify('firefox'), __MV__: 2 })Anything that compiles this source with its own bundler must define all three,
or the bundle ships a reference to an undefined global and throws at runtime.
LiveTL is the one such consumer: it builds chat-background.ts,
chat-injector.ts, chat-interceptor.ts, hyperchat.ts and options.ts as its
own entry points, so it needs these in both its webpack (MV2) and vite (MV3)
configs. __MV__ must match that consumer's own manifest version, not ours.
Used by WelcomeMessage.svelte, Hyperchat.svelte, HyperchatButton.svelte,
chat-background.ts, chat-injector.ts. When adding a new constant, update this
table — a missing define fails at runtime, not at build time.
- Prefer editing existing modules and utilities over creating one-off files for tiny helpers.
- If a helper obviously belongs in an existing shared utility file, put it there.
- Keep MV2 adaptation narrow:
- change only what is required for manifest/background/injection differences
- reach for
__MV__only when an API genuinely differs between manifest versions
- Prefer render-edge formatting over mutating raw identity data:
- keep parsed message/channel ids untouched
- transform display text at component or view-model boundaries
- Prefer resilient lookups over brittle positions:
- endpoint/type detection over fixed menu indices
- semantic selectors/utilities over DOM-order assumptions
- When a bug appears in multiple surfaces, prefer fixing the shared parser/messaging/util layer before patching several components by hand.
- Keep release bullets short and user-facing.
- Prefer active voice:
Fix admin block/report actionsHide leading @ in names
- Avoid passive voice, filler, and overly technical internal wording unless the release note is specifically for maintainers.
- Run
scripts/codex-dev.sh setup-mcponce (or per fresh machine) to register the Codex MCP server:- name:
chrome-devtools - command:
npx -y chrome-devtools-mcp@latest --browserUrl=http://127.0.0.1:9222
- name:
- Use
scripts/codex-dev.sh watchonce per session to keep Chrome extension builds live in the background. - The watcher resolves to MV3 Chrome scripts (
dev:chrome/build:chrome) andbuild/chromeoutput automatically. - The harness is Chromium-only. The MV2 target is Firefox-only, so
go-testdoes not cover it — validate MV2 by loadingbuild/mv2in Firefox by hand. - Start headless browser testing only when explicitly requested (for example: "go test", "test this", "run browser test").
- For test runs, use
scripts/codex-dev.sh go-test. This guarantees:- MCP configuration is present
- watcher is running
- headless Chromium is restarted with a fresh profile and extension reload
- If Chromium fails to start in a sandboxed/snap environment, set
CHROME_BINto a non-snap Chrome/Chromium binary beforego-test.
- After significant extension-runtime changes, run
scripts/codex-dev.sh reloadbefore validation. - Treat these as significant by default:
src/scripts/**src/components/**src/manifest.jsonvite.config.ts- settings/storage/messaging code under
src/ts/**
- The reload is intentionally hard (full browser restart) to avoid stale MV3 service-worker state, extension cache artifacts, and mixed-profile debugging drift.
- For chat author display, hide a leading
@in UI text while keeping underlying identity data unchanged. - Use
src/ts/component-utils.ts(formatAuthorName) for this transformation and apply it at render points.
- Treat legacy member emoji placeholders (
U+25A1, rendered as□) as emoji-equivalent for filtering. - In
HIDE_ALLmode, do not render these placeholders inMessageRuns.svelte. - For emoji-only spam detection, count placeholder-only text runs as emoji in
isAllEmoji.
- Do not assume fixed menu item indices from
get_item_context_menu(YouTube may reorder menu items). - Resolve block/report actions by searching for endpoint types (
moderateLiveChatEndpoint,getReportFormEndpoint) in the response tree. - Always post
chatUserActionResponseeven when message context params are missing so UI state can fail gracefully. - Keep proxy fetch request/response events correlated by request id; do not use unscoped global listeners.
- For deeper notes on implementing new YouTube chat actions (headers, tracking params, endpoint discovery, SAPISIDHASH, and debugging), see
docs/YOUTUBE_ACTIONS.md.
- Headless validation should open the same
startUrlused byvite.config.ts. scripts/codex-dev.sh go-testdoes this automatically (defaulting by detected mode), andTEST_URLcan override when needed.
- Always rebuild for the target browser before runtime validation:
yarn build:chromeyarn build:firefox
- Chromium extension validation is most reliable in CI/headless shells with:
- Playwright Chromium persistent context
headless=falseplus--ozone-platform=headless- extension args:
--disable-extensions-except=<build>and--load-extension=<build>
- Firefox validation in this environment must set
HOME=/rootbefore launching browser automation as root, or Firefox exits early with a root/session ownership error. - For Firefox runtime checks, prefer
https://www.youtube.com/live_chat?is_popout=1&v=X4VbdwhkE10&continuation=0ofMyAOAARpeQ2lrcUp3b1lWVU5UU2pSbmExWkROazV5ZGtsSk9IVnRlblJtTUU5M0VndFlORlppWkhkb2EwVXhNQm9UNnFqZHVRRU5DZ3RZTkZaaVpIZG9hMFV4TUNBQk1BQSUzRDABggEICAQYAiAAKACIAQGgAfr808_a-JQDqAEAsgEAfor deterministic chat-frame loading in headless mode. - Packaged LiveTL Firefox translation is a special case: keep the request bridge in HC, but host the actual translator iframe on the YouTube page side.
- For LiveTL MV2 (webpack),
iframe-translator'sgetClient()is safe to use as long as the bundler rewritesimport.meta.env.DEVtofalsefornode_modules/iframe-translator/index.js(otherwiseimport.meta.envcan be undefined at runtime).
- The MV3 embed fallback page (
/embed/hyperchat_embed) can render a centered YouTube logo/error artifact if page elements are not fully removed. - In
src/scripts/chat-mounter.ts, treat the HyperChat mount root as the only allowed directbodychild and aggressively remove fallback embed artifacts, including#player-controls. - If the logo reappears in browser tests, prioritize checking
chat-mounter.tscleanup selectors and page timing behavior before touching parser/UI code.
scripts/codex-dev.sh statusshows watcher/MCP/browser states.scripts/codex-dev.sh logsprints watcher/browser log file locations.scripts/codex-dev.sh stopshuts down watcher and the headless browser.