Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 61 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
# HyperChat Codex Workflow (MV3 Main)
# HyperChat Codex Workflow

## Branch Discipline

- Make code changes on `mv2` first.
- `main` is the real MV3 branch.
- `mv3` and `mv3-ltl` are retired. Do not use them as MV3 source branches for new work or cross-repo sync; `main` is the MV3 line LiveTL consumes.
- Do not implement feature/fix work directly on `main`.
- If a task starts on another branch, switch to `mv2` before editing unless the user explicitly asks otherwise.
- `main` is the only maintained source branch. Make code changes there.
- `mv3` and `mv3-ltl` are retired. The remote `mv2` branch is a temporary rollback reference until LiveTL migration and runtime validation finish; do not use it for new work or delete it early.
- 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:
1. HyperChat `mv2`
2. HyperChat `main`
3. LiveTL `develop`
4. LiveTL `mv3-fr`
5. LiveTL `release`
1. HyperChat `main`
2. LiveTL `develop`
3. LiveTL `mv3-fr`
4. LiveTL `release`
- 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 `main` is updated:
- merge HyperChat `main` into YTCF `master`
Expand All @@ -28,17 +25,63 @@
- `order 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.
- Prefer proper merges when carrying `mv2` work into `main`.
- If MV3 needs follow-up adaptation, keep that as a small, explicit commit after the merge instead of rewriting history or hand-copying changes.

## Manifest Version Targets

- `main` builds three targets: `chrome` (MV3), `firefox` (MV3), and `mv2` (MV2, Firefox-only).
- The `mv2` target is **not** legacy cruft. Firefox's MV3 support is unreliable for LiveTL's needs, so LiveTL's Firefox variant will consume it after the downstream migration. Do not "simplify" it away.
- Keep the MV2/MV3 distinction in exactly two places:
- `src/manifest.json` — `{{mv2}}.` / `{{mv3}}.` key prefixes, resolved by `scripts/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.ts` and friends). `main`'s architecture — thin background plus the broker in `src/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. `mv2` is built in CI for breakage coverage and consumed by LiveTL via submodule.

## Build-Time Constants (submodule consumers)

`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:

```js
// 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.

## Code Patterns

- 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.
- Put shared behavior on `mv2` first; `main` should usually be a merge-plus-adaptation branch, not a separate feature branch.
- Keep MV3 adaptation narrow:
- preserve branch-specific build/runtime wiring
- 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
Expand All @@ -61,8 +104,8 @@
- name: `chrome-devtools`
- command: `npx -y chrome-devtools-mcp@latest --browserUrl=http://127.0.0.1:9222`
- Use `scripts/codex-dev.sh watch` once per session to keep Chrome extension builds live in the background.
- On `main`, this resolves to MV3 scripts (`dev:chrome`/`build:chrome`) and `build/chrome` output automatically.
- On `mv2`, watcher mode prefers `npm run start:none` so build watch stays alive without separate browser autolaunch.
- The watcher resolves to MV3 Chrome scripts (`dev:chrome`/`build:chrome`) and `build/chrome` output automatically.
- The harness is Chromium-only. The MV2 target is Firefox-only, so `go-test` does not cover it — validate MV2 by loading `build/mv2` in 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
Expand Down
39 changes: 30 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,26 @@ See https://livetl.app/hyperchat/install

## Building from Source

### ⚠️ WARNING ⚠️
### Build targets

For legacy reasons, we have a `mv2` branch used by [the LiveTL extension](https://github.com/LiveTL/LiveTL)'s Manifest V2 Firefox variant, while the `main` branch houses the main Manifest V3 version that's published to stores.
Everything ships from `main`. There is one source tree and three build targets:

> Transition status: migrating both LiveTL lines to this source and deleting the
> remote `mv2` rollback branch are still pending runtime validation.

| Target | Manifest | Consumer |
| --------- | ----------------- | -------------------------------------------------------------------------------------- |
| `chrome` | MV3 | Chrome Web Store |
| `firefox` | MV3 | Firefox Add-ons |
| `mv2` | MV2, Firefox-only | [The LiveTL extension](https://github.com/LiveTL/LiveTL)'s Manifest V2 Firefox variant |

> ⚠️ The `mv2` target is **not** legacy cruft. Firefox's MV3 support is unreliable
> for LiveTL's needs, so LiveTL's Firefox variant consumes the MV2 build.

MV2/MV3 differences live in two places only: `{{mv2}}.` / `{{mv3}}.` prefixed keys
in `src/manifest.json` (resolved by `scripts/resolve-manifest.ts`), and `__MV__`
checks in source, which are build-time constants so each bundle carries only its
own target's code.

### Development

Expand All @@ -41,8 +58,9 @@ npm install # install dependencies
Serve the extension for local development:

```bash
npm run dev:chrome # devserver for Chrome extension
npm run dev:firefox # devserver for Firefox extension
npm run dev:chrome # devserver for Chrome extension (MV3)
npm run dev:firefox # devserver for Firefox extension (MV3)
npm run dev:mv2 # devserver for the Firefox MV2 variant

npm run start:chrome # devserver + open extension in Chrome
npm run start:firefox # devserver + open extension in Firefox
Expand All @@ -55,13 +73,16 @@ Our build script is [an automated GitHub action](.github/workflows/release.yml),
To simulate the build:

```bash
VERSION=X.Y.Z npm run build # Chrome & Firefox
VERSION=X.Y.Z npm run build:chrome # just Chrome
VERSION=X.Y.Z npm run build:firefox # just Firefox
VERSION=X.Y.Z npm run build # all three targets
VERSION=X.Y.Z npm run build:chrome # just Chrome (MV3)
VERSION=X.Y.Z npm run build:firefox # just Firefox (MV3)
VERSION=X.Y.Z npm run build:mv2 # just Firefox MV2
```

The built ZIP files can be found in the `build` directory.
The built ZIP files can be found in the `build` directory. Only the two MV3 zips
are published as release assets; the MV2 build is verified in CI and consumed by
LiveTL through a git submodule.

## Release

Release steps for `mv2` and `main` are documented in [RELEASE_PROCESS.md](./RELEASE_PROCESS.md).
Release steps are documented in [RELEASE_PROCESS.md](./RELEASE_PROCESS.md).
96 changes: 50 additions & 46 deletions RELEASE_PROCESS.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,80 @@
# HyperChat Release Process

This repo ships from two active lines that serve different purposes.
Everything ships from `main`. There is no branch ladder — MV2 and MV3 are build
targets, not branches.

## Branch Order
`mv3` and `mv3-ltl` are historical. The remote `mv2` branch is rollback-only
until LiveTL migration and runtime validation finish; do not route maintenance
through it or delete it early.

1. Make and validate changes on `mv2` first.
2. Commit and push on `mv2`.
3. Merge `mv2` into `main`.
4. Validate and push `main`.
5. Create releases from each branch that actually ships.
## Branch Order

`main` is the MV3 line LiveTL consumes. `mv3` and `mv3-ltl` are historical. Do not route normal maintenance through them unless the user explicitly asks for branch archaeology.
1. Make and validate changes on `main`.
2. Push `main`.
3. Create and publish the release tag.

If the same task also affects LiveTL, do not begin in LiveTL. Finish this HyperChat ladder first, then bump the LiveTL submodule chain in this order:
If the same task also affects LiveTL, do not begin in LiveTL. Finish HyperChat
first, then bump the LiveTL submodule chain in this order:

1. LiveTL `develop`
2. LiveTL `mv3-fr`
3. LiveTL `release`

HyperChat is the upstream source for shared chat behavior. LiveTL is downstream packaging/integration work after that.
HyperChat is the upstream source for shared chat behavior. LiveTL is downstream
packaging/integration work after that.

## Local Validation

On `mv2`:

```bash
git checkout mv2
yarn
yarn build
yarn package
npm ci
VERSION=0.0.0 npm run build # chrome + firefox (MV3) + mv2, in parallel
```

On `main`:

```bash
git checkout main
yarn
yarn build:chrome
yarn build:firefox
```
Individual targets are `build:chrome`, `build:firefox`, and `build:mv2`. See
[README.md](./README.md) for what each one is for.

Reinstall after switching branches. Dependency trees and lockfiles differ enough that stale installs can break builds.
Reinstall after switching branches — stale installs can break builds, and the
artifact comparison below is only valid against the `node_modules` that produced
the baseline.

## Release Behavior By Branch

### `mv2`
## Release Behavior

- Workflow: `.github/workflows/release.yml`
- Trigger: GitHub release event `released`
- Build command uses inline env injection: `VERSION=<tag> yarn build`
- Result: release tag version is injected into the built manifest at build time.
- Trigger: GitHub release event `published`, or `workflow_dispatch` with a tag
- Version comes from the **release tag**, not `package.json`: the workflow strips
the leading `v` and any suffix, then passes it as `VERSION` so Vite writes it
into each manifest at build time.

Practical rule for `mv2`: create a release tag like `v69.43.0` and the workflow builds that version.
Practical rule: create a release tag like `v3.3.0` and publish it. The workflow
builds that version.

### `main`
Published assets:

- Workflow: `.github/workflows/release.yml`
- Trigger: GitHub release event `published`
- Build version comes from `package.json`, and Vite writes it into the manifest at build time.
- `HyperChat-Chrome.zip` — MV3, Chrome Web Store
- `HyperChat-Firefox.zip` — MV3, Firefox Add-ons

The MV2 target is **not** published as a release asset. It is built on every push
by `.github/workflows/build.yml` (via `npm run build`) so breakage is caught, and
LiveTL consumes it through a git submodule rather than a download.

Practical rule for `main`: bump `package.json`, commit it, then create/publish the release tag.
## Verifying A Build

## Recommended "Least Surprise" Publish Flow
There is no test suite; build output is the oracle. To check that a change did
not move the shipped artifacts, capture a baseline before the change and compare
byte-for-byte after — file lists and sizes miss same-size changes:

1. Finish feature/fix on `mv2`, run local build/package checks, push.
2. Merge to `main`, run local build checks, push.
3. Create `mv2` release tag and publish release.
4. Bump version state on `main`, then create/publish the `main` release tag.
5. Confirm both artifacts exist on each GitHub release:
- `HyperChat-Chrome.zip`
- `HyperChat-Firefox.zip`
```bash
python3 -c "
import zipfile
for b in ['HyperChat-chrome.zip','HyperChat-firefox.zip']:
a=zipfile.ZipFile('/tmp/golden/'+b); c=zipfile.ZipFile('build/'+b)
d=[n for n in sorted(set(a.namelist())) if a.read(n)!=c.read(n)]
print(b, d or 'all bytes match')
"
```

## Notes For LiveTL Sync

LiveTL consumes HyperChat via submodules. Keep HyperChat changes branch-aligned across `mv2` and `main` so LiveTL can take them with straightforward submodule bumps in its release flow. LiveTL's MV2 Firefox variant tracks `mv2`; its MV3 line tracks `main`.
LiveTL consumes HyperChat via git submodules, not release assets. Its MV2 Firefox
variant and MV3 line will both track `main` after the downstream migration. Until
their release pins and runtime checks pass, keep the remote `mv2` rollback branch.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"build": "npm run typecheck && npm-run-all --parallel build:chrome build:firefox",
"build": "npm run typecheck && npm-run-all --parallel build:chrome build:firefox build:mv2 && npm run verify:build",
"build:chrome": "cross-env BROWSER=chrome vite build",
"build:firefox": "cross-env BROWSER=firefox vite build",
"build:mv2": "cross-env BROWSER=firefox MV=2 vite build",
"dev:chrome": "cross-env MINIFY=false BROWSER=chrome vite build --watch",
"dev:firefox": "cross-env MINIFY=false BROWSER=firefox vite build --watch",
"dev:mv2": "cross-env MINIFY=false BROWSER=firefox MV=2 vite build --watch",
"verify:build": "node scripts/verify-build.mjs",
"start": "npm run dev:chrome",
"start:none": "npm run dev:chrome",
"start:chrome": "cross-env AUTOLAUNCH=true npm run dev:chrome",
Expand Down
26 changes: 26 additions & 0 deletions scripts/resolve-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Resolves `{{mv2}}.` / `{{mv3}}.` key prefixes in the manifest against the
* target manifest version, dropping keys tagged for the other one.
*
* Browser tags (`{{chrome}}.` / `{{firefox}}.`) are deliberately left in place:
* vite-plugin-web-extension resolves those itself from its `browser` option.
*
* Both resolvers only match a *leading* prefix, so the MV tag has to come first
* — `{{mv3}}.{{firefox}}.background`, never `{{firefox}}.{{mv3}}.background`.
* The reverse order leaves a literal `{{mv3}}.` key in the emitted manifest.
*/

const mvTag = /^\{\{mv([23])\}\}\./;

export const resolveMv = (value: any, mv: 2 | 3): any => {
if (Array.isArray(value)) return value.map(item => resolveMv(item, mv));
if (typeof value !== 'object' || value === null) return value;

const resolved: Record<string, any> = {};
for (const [key, child] of Object.entries(value)) {
const tag = key.match(mvTag);
if (tag && parseInt(tag[1], 10) !== mv) continue;
resolved[tag ? key.slice(tag[0].length) : key] = resolveMv(child, mv);
}
return resolved;
};
Loading
Loading