Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d0b3ee7
feat(core-beta): tell the user once when a beta feature turns on
synap5e Sep 21, 2026
b57efe2
feat(core-beta): let the PostHog payload word the activation notice
synap5e Sep 21, 2026
f0d0297
fix(core-beta): address review findings on the activation notice
synap5e Sep 21, 2026
142d39b
Merge #1551 review fixes, and address this PR's own review findings
synap5e Sep 21, 2026
eb4a944
fix(core-beta): second review round — drop the cache, harden the popu…
synap5e Sep 21, 2026
b167d70
Merge #1551 round-2 review fixes
synap5e Sep 21, 2026
d077eaf
fix(core-beta): coalesce a turned-away show, key the latch per install
synap5e Sep 21, 2026
0efd88b
Merge #1551 round-3 review fixes
synap5e Sep 21, 2026
689d24e
fix(core-beta): refuse a malformed shownArgs instead of filtering it …
synap5e Sep 21, 2026
1121186
Merge #1551: refuse malformed shownArgs
synap5e Sep 21, 2026
5e6ced9
fix(core-beta): adapt the merged shownArgs test to this branch's gran…
synap5e Sep 21, 2026
ff832a6
fix(core-beta): resolve notice copy at show time, not at setup
synap5e Sep 21, 2026
0af3a08
fix(core-beta): reserve the named spec's port, honour the null IPC co…
synap5e Sep 21, 2026
ad08a8f
Merge #1551: resolve notice copy at show time
synap5e Sep 21, 2026
ba10ca6
fix(core-beta): reconcile the card when main hides it out from under us
synap5e Sep 21, 2026
a261653
Merge #1551: reconcile the card when main hides it
synap5e Sep 21, 2026
35f146c
fix(core-beta): adapt the merged auto-hide test to this branch's noti…
synap5e Sep 21, 2026
79739f0
test(core-beta): pin that a retired card is not resurrected by its ow…
synap5e Sep 21, 2026
bdbdca7
Merge #1551: pin the retire/auto-hide invariant
synap5e Sep 21, 2026
bbd33dd
fix(core-beta): re-show after movement, and drop a claim the launch n…
synap5e Sep 21, 2026
5d13478
Merge #1551: re-show after movement, drop unearned claims
synap5e Sep 21, 2026
ce0b6ea
fix(core-beta): tell the onboarding hint about its auto-hide too
synap5e Sep 21, 2026
df3b090
Merge #1551: route the pill hint's auto-hide too
synap5e Sep 21, 2026
8a2508a
fix(core-beta): hand a released claim to the install that lost the race
synap5e Sep 21, 2026
302037d
Merge #1551: hand a released claim to the losing install
synap5e Sep 21, 2026
351e808
refactor(core-beta): drop cross-install claim suppression, keep the a…
synap5e Sep 21, 2026
a391b3a
Merge #1551: drop cross-install claim suppression
synap5e Sep 21, 2026
6997d44
fix(core-beta): clear the claim at the guarded-setup chokepoint
synap5e Sep 21, 2026
d8b4b02
Merge #1551: clear the claim at the guarded-setup chokepoint
synap5e Sep 21, 2026
7a98bf1
Merge #1551: clear the claim at the guarded-setup chokepoint
synap5e Sep 21, 2026
f6ca0f5
fix(core-beta): filter announced args at read, not only at arm
synap5e Sep 21, 2026
2a23c42
Merge #1551: filter announced args at read
synap5e Sep 21, 2026
68231a5
fix(core-beta): debounce the re-show in main, where every move is vis…
synap5e Sep 21, 2026
c88c798
Merge #1551: debounce the re-show in main
synap5e Sep 21, 2026
bcac4cd
fix(core-beta): tell a card when the other one takes the popup
synap5e Sep 21, 2026
4d351d9
Merge #1551: tell a card when the other one takes the popup
synap5e Sep 21, 2026
87572b1
Merge #1551: tell a card when the other one takes the popup
synap5e Sep 21, 2026
7041e29
test(core-beta): pin that first-use lockdown only defers the notice
synap5e Sep 21, 2026
4d349f1
Merge remote-tracking branch 'origin/synap5e/feat/beta-activation-not…
synap5e Sep 21, 2026
8c635db
Merge remote-tracking branch 'origin/main' into synap5e/feat/beta-act…
synap5e Sep 21, 2026
b4c0741
Merge remote-tracking branch 'origin/synap5e/feat/beta-activation-not…
synap5e Sep 21, 2026
1eaa22b
chore(core-beta): name the feature "Asset library" in the notice fixt…
synap5e Sep 21, 2026
d0b79c4
docs(i18n): write down the {feature} placeholder contract
synap5e Sep 21, 2026
94b2b22
docs(i18n): namespace all four keys in the placeholder contract
synap5e Sep 22, 2026
f1cad37
Merge remote-tracking branch 'origin/main' into synap5e/feat/beta-act…
synap5e Sep 22, 2026
5e100fe
Merge remote-tracking branch 'origin/synap5e/feat/beta-activation-not…
synap5e Sep 22, 2026
7520949
Merge main into synap5e/feat/beta-notice-payload-wording after #1551 …
synap5e Sep 22, 2026
2b6ac0a
test(core-beta): make the anchor assertion say WHICH term is wrong
synap5e Sep 22, 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
137 changes: 137 additions & 0 deletions e2e/beta-activation-notice-named.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* E2E: payload-controlled wording for the Core beta activation notice.
*
* The companion spec (`beta-activation-notice.test.ts`) covers the generic card and the whole
* trigger path. This one covers only what the PostHog payload adds: a `description` reaching
* the card as a feature name, through the real parser and the real launch.
*
* Tagged `@linux` only, for the two reasons documented in `fakeComfyInstall.ts`: the
* interpreter stub cannot be a PE executable on Windows, and nothing isolates `userData` on
* macOS, so the ops-flag seed would hit the real profile.
*
* Run: `pnpm exec playwright test --project=linux e2e/beta-activation-notice-named.test.ts`
*/
import os from 'node:os'
import path from 'node:path'
import { mkdir, mkdtemp, rm } from 'node:fs/promises'
import { expect, test, type ElectronApplication } from '@playwright/test'
import { launchApp, type AppContext } from './launchApp'
import { clickInstallTile, expectChooserVisible } from './support/chooserHelpers'
import { WebContentsPage } from './support/cdpPages'
import { opsFlagsGrantSeed, reserveFreePort, writeFakeComfyInstall } from './support/fakeComfyInstall'
import { captureHostWindow } from './support/windowCapture'

// A real launch does not fit the default 45s budget.
test.describe.configure({ mode: 'serial', timeout: 180_000 })

const INSTALL_ID = 'inst-beta-notice-named'
const INSTALL_NAME = 'Named Beta Fixture'
const GRANT_ARG = '--enable-assets'
const GRANT_MIN_CORE = '0.3.80'
/** What the payload calls the feature. Deliberately not derivable from the arg token, so a
* card showing it proves the payload reached the copy rather than a table in Desktop. */
const FEATURE_NAME = 'Asset library'

let ctx: AppContext
let installPath: string
let port: number
let previousPosthogHost: string | undefined

/** See the companion spec: a closed port makes the flag fetch `unreachable`, which is the
* documented path where the persisted `ops-flags.json` is authoritative. */
const UNREACHABLE_POSTHOG_HOST = 'http://127.0.0.1:1'

function coachmarkPopup(app: ElectronApplication): WebContentsPage {
return new WebContentsPage(app, 'comfyTitleTooltip')
}

test.beforeAll(async () => {
previousPosthogHost = process.env['POSTHOG_HOST']
process.env['POSTHOG_HOST'] = UNREACHABLE_POSTHOG_HOST

installPath = await mkdtemp(path.join(os.tmpdir(), 'comfyui-beta-notice-named-'))
port = await reserveFreePort()
await writeFakeComfyInstall({ installPath, port })

ctx = await launchApp({
settings: {
firstUseCompleted: true,
telemetryEnabled: true,
betaFeaturesEnabled: true,
hasSeenCentralPillHint: true
},
installations: [
{
id: INSTALL_ID,
name: INSTALL_NAME,
sourceId: 'comfybuilder',
sourceLabel: 'ComfyBuilder',
installPath,
status: 'installed',
launchArgs: `--port ${port}`,
launchMode: 'window',
browserPartition: 'unique',
seen: true,
comfyVersion: {
commit: 'b1c2d3e4f5a6b1c2d3e4f5a6b1c2d3e4f5a6b1c2',
baseTag: 'v0.3.99',
commitsAhead: 0,
baseTagVerified: true
}
}
],
// Env-delivered so main writes it to the real `configDir()`; the harness cannot place
// that file on macOS, where `userData` ignores the HOME override.
opsFlags: opsFlagsGrantSeed({
arg: GRANT_ARG,
minCoreVersion: GRANT_MIN_CORE,
description: FEATURE_NAME
})
})
await expectChooserVisible(ctx.panel)
})

test.afterAll(async () => {
await ctx?.cleanup()
if (installPath) await rm(installPath, { recursive: true, force: true })
if (previousPosthogHost === undefined) delete process.env['POSTHOG_HOST']
else process.env['POSTHOG_HOST'] = previousPosthogHost
})

test('a payload-supplied feature name reaches the card @linux', async () => {
await clickInstallTile(ctx.panel, INSTALL_NAME)

await ctx.panel.waitFor(
async () =>
(await ctx.app.evaluate(
({ webContents }, port) =>
webContents.getAllWebContents().some((wc) => wc.getURL().includes(String(port))),
port
)) === true,
{ timeout: 90_000, message: 'ComfyUI stub never came up / the host never attached' }
)

const popup = coachmarkPopup(ctx.app)
await popup.waitFor(
async () => {
try {
return await popup.exists('.coachmark')
} catch {
return false
}
},
{ timeout: 30_000, message: 'beta activation notice never appeared' }
)

expect(await popup.textOf('.coachmark-title')).toBe(`The ${FEATURE_NAME} beta is on`)
expect(await popup.textOf('.coachmark-text')).toContain(FEATURE_NAME)
// Naming the feature must not cost the card its way out.
expect(await popup.textOf('.coachmark-action')).toBe('Settings')
// The raw token still never reaches the user.
expect(await popup.textOf('.coachmark-text')).not.toContain(GRANT_ARG)

const shotPath = path.join(test.info().outputDir, '03-notice-named-by-payload.png')
await mkdir(path.dirname(shotPath), { recursive: true })
const shot = await captureHostWindow(ctx.app, ctx.panel, shotPath)
test.info().attach('named notice', { path: shot, contentType: 'image/png' })
})
33 changes: 30 additions & 3 deletions e2e/beta-activation-notice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,31 @@ test('the card is anchored on the bell it points at @linux', async () => {
const b = beak.getBoundingClientRect()
return (b.left + b.right) / 2
})()`)

/** Everything the offset is computed from, for the failure message.
*
* A bare "expected <= 2, received 8" cannot say WHICH term is wrong, and this assertion
* has already failed on a runner where it passes locally at the same window size. The
* offset is `view.x + beak - bell`, so a failure is one of: the view not centred on the
* bell, the card not centred in the view, or the beak not centred in the card. Each has a
* different cause and a different fix, and one CI failure should be enough to tell them
* apart instead of costing another round trip. */
const geometry = async (): Promise<string> => {
const card = await coachmarkPopup(ctx.app).evaluate<string>(`(() => {
const c = document.querySelector('.coachmark')
const b = document.querySelector('.coachmark-beak')
if (!c || !b) return 'card=<absent>'
const cr = c.getBoundingClientRect(), br = b.getBoundingClientRect()
return 'cardLeft=' + cr.left + ' cardWidth=' + cr.width +
' beakInCard=' + ((br.left + br.right) / 2 - cr.left) +
' beakInlineStyle=' + (b.style.left || '<none>') +
' pageWidth=' + window.innerWidth
})()`)
return (
`bell=${bellCentre} viewX=${popup!.x} viewWidth=${popup!.width} ${card}` +
` | viewCentreVsBell=${popup!.x + popup!.width / 2 - bellCentre}`
)
}
expect(await beakCentre(), 'no beak found on the card').toBeGreaterThanOrEqual(0)

// When nothing clamped it, the beak must land ON the bell. Asserted only in the unclamped
Expand All @@ -247,7 +272,9 @@ test('the card is anchored on the bell it points at @linux', async () => {
await expect
.poll(async () => Math.abs(popup!.x + (await beakCentre()) - bellCentre), {
timeout: 10_000,
message: 'the beak must point at the bell, not merely sit in a view that is centred on it',
message:
Comment thread
christian-byrne marked this conversation as resolved.
'the beak must point at the bell, not merely sit in a view that is centred on it. ' +
(await geometry()),
})
.toBeLessThanOrEqual(2)
}
Expand Down Expand Up @@ -308,10 +335,10 @@ test('the settings link lands on the beta opt-in row and retires the card @linux
test('the notice is spent: a second launch stays silent @linux', async () => {
// The whole point of persisting on retire rather than on show. Read through the same IPC
// the title bar uses, so this asserts the contract the renderer actually depends on.
const stillPending = await ctx.titleBar.evaluate<string[]>(
const stillPending = await ctx.titleBar.evaluate<unknown>(
`window.api.getPendingBetaNotice(${JSON.stringify(INSTALL_ID)})`,
)
expect(stillPending).toEqual([])
expect(stillPending).toBeNull()

const announced = await ctx.titleBar.evaluate<unknown>(
`window.api.getSetting('betaNoticeAnnouncedArgs')`,
Expand Down
15 changes: 14 additions & 1 deletion e2e/support/fakeComfyInstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,24 @@ export async function writeFakeComfyInstall(opts: {
export function opsFlagsGrantSeed(opts: {
arg: string
minCoreVersion: string
/** Optional per-flag notice wording, written in the payload's own wire shape so the fixture
* exercises the real parser rather than the already-parsed type. */
description?: string
notice?: 'silent'
}): Record<string, unknown> {
return {
desktop_core_beta_features: {
value: 'treatment',
payload: { flags: [{ arg: opts.arg, min_core_version: opts.minCoreVersion }] },
payload: {
flags: [
{
arg: opts.arg,
min_core_version: opts.minCoreVersion,
...(opts.description === undefined ? {} : { description: opts.description }),
...(opts.notice === undefined ? {} : { notice: opts.notice }),
},
],
},
},
}
}
37 changes: 37 additions & 0 deletions locales/drafts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,40 @@ They are kept here as a starting point for future translation work. To activate
a translation, bring its file up to date with `en.json` (ensure all keys are
present and correctly translated), then move it back into the parent `locales/`
directory. The launcher auto-discovers any `.json` file in `locales/`.

## Placeholder contract

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recalculate the final change breakdown

Fresh evidence in this reviewed commit shows the breakdown has drifted again: git diff --numstat 4bed2b5^ 4bed2b5 reports 20 files, 935 additions, 171 deletions, and 1,106 changed lines, but the description reports 19 files and 1,054 lines. This newly added documentation file is omitted entirely, despite documentation requiring its own category; the product totals are also understated at +280/−77 (357 lines), while tests are +607/−94 (701), localization is +12/−0, and documentation is +36/−0. Update the file lists, category shares, and overall totals to match the reviewed commit.

AGENTS.md reference: AGENTS.md:L3-L7

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, and your numbers reproduce exactly: 20 files, +935/−171 = 1106; product 9 files +280/−77 = 357 (32.3%), tests 8 files +607/−94 = 701 (63.4%), documentation 1 file +36/−0 = 36 (3.3%), localization 2 files +12/−0 = 12 (1.1%).

You caught two distinct things and the second is the more useful one.

The drift itself was mine: I regenerate the table from the diff, and I pushed the documentation commit after regenerating, which makes the table describe the previous head. That is the third time this has happened on this PR, always the same way round.

The missing Documentation category was a real defect in how I generate it, not just a stale run. My categoriser tested the locales/ prefix before the file extension, so locales/drafts/README.md — a Markdown file that happens to live under locales/ — was silently absorbed into the localization bucket instead of getting the separate row AGENTS.md asks for. Any future .md under a directory the categoriser recognised would have vanished the same way. The extension check now runs first, so the category is derived from what the file is rather than from where it sits.


Some strings interpolate a value, written `{likeThis}`. A placeholder is **not
always a word of the sentence's own language**, and translating around one
requires knowing which kind it is.

### `{feature}` — the beta-notice keys

`titleBar.betaNoticeTitleNamed`, `titleBar.betaNoticeBodyNamed`,
`titleBar.betaNoticeOffTitleNamed` and `titleBar.betaNoticeOffBodyNamed`
interpolate `{feature}`:
the name of a beta feature, e.g. `Asset library`.

It is an **opaque proper name**. It is supplied at runtime by a remote
configuration payload, it is the same string for every user regardless of
locale, and it is **English today**. Desktop cannot translate it and cannot
know its grammatical gender or number.

So, when translating these four strings:

- **Keep `{feature}` a modifier, never the grammatical head.** In English it
modifies a constant head noun — "The *{feature}* **beta** is on" — and it is
that head noun ("beta") the sentence agrees with. Keep an equivalent constant
head in your language and let it carry the agreement: *la bêta {feature}*,
*die {feature}-Beta*, *бета-функция {feature}*.
- **Do not make the sentence agree with, decline, or inflect `{feature}`.** A
form like "{feature} est activé" or "{feature} включён" needs the name's
gender, which is unknowable — it would be guessing, and the guess changes
whenever ops names a new feature.
- **Word order is yours.** Putting the name after the head noun instead of
before it is expected, not a problem.
- An article immediately before `{feature}` should agree with the head noun,
not with the name.

If your language cannot express this without inflecting the name, say so rather
than picking a gender — the template needs changing, not the translation.
6 changes: 6 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
"pillHintDismiss": "Got it",
"betaNoticeTitle": "A beta feature is on",
"betaNoticeBody": "This instance started with a beta feature enabled. You can turn beta features off in Settings.",
"betaNoticeTitleNamed": "The {feature} beta is on",
"betaNoticeBodyNamed": "This instance started with the {feature} beta enabled. You can turn beta features off in Settings.",
"betaNoticeOffTitle": "A beta feature is off",
"betaNoticeOffBody": "We've turned a beta feature off for this instance. You can manage beta features in Settings.",
"betaNoticeOffTitleNamed": "The {feature} beta is off",
"betaNoticeOffBodyNamed": "We've turned the {feature} beta off for this instance. You can manage beta features in Settings.",
"betaNoticeDismiss": "Got it",
"betaNoticeSettings": "Settings"
},
Expand Down
6 changes: 6 additions & 0 deletions locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
"pillHintDismiss": "知道了",
"betaNoticeTitle": "已启用 Beta 功能",
"betaNoticeBody": "此实例启动时启用了一项 Beta 功能。你可以在设置中关闭 Beta 功能。",
"betaNoticeTitleNamed": "已启用 {feature} Beta 功能",
"betaNoticeBodyNamed": "此实例启动时启用了 {feature} Beta 功能。你可以在设置中关闭 Beta 功能。",
"betaNoticeOffTitle": "已关闭一项 Beta 功能",
"betaNoticeOffBody": "我们已为此实例关闭了一项 Beta 功能。你可以在设置中管理 Beta 功能。",
"betaNoticeOffTitleNamed": "已关闭 {feature} Beta 功能",
"betaNoticeOffBodyNamed": "我们已为此实例关闭了 {feature} Beta 功能。你可以在设置中管理 Beta 功能。",
"betaNoticeDismiss": "知道了",
"betaNoticeSettings": "设置"
},
Expand Down
Loading
Loading