Skip to content

Sync main: packaging fixes (v0.1.1-v0.1.3) + README rewrite - #20

Open
richardosborne14 wants to merge 23 commits into
mainfrom
cline-dev
Open

Sync main: packaging fixes (v0.1.1-v0.1.3) + README rewrite#20
richardosborne14 wants to merge 23 commits into
mainfrom
cline-dev

Conversation

@richardosborne14

Copy link
Copy Markdown

Summary

  • Three packaging fixes that took v0.1.1's blank-launcher-screen crash to a working v0.1.3 (eslint-linter-browserify, globals, lodash — all the same webpack-externals/electron-builder-pruning gap across a workspace boundary).
  • README rewritten for NodeGX's actual current state (real Discord, live docs site, real backend model) instead of the pre-fork OpenNoodl/Noodl split.
  • Phase-33 session log entry.

v0.1.3 is already built, signed, notarized, and published as the release — this PR just brings the commits that produced it onto main.

🤖 Generated with Claude Code

richardosborne14 and others added 23 commits August 7, 2026 16:12
…, D4 correction

Full account in PROGRESS.md's new 2026-08-07 (fourth session) entry and in
memory (main-was-never-synced-2026-08-07). Short version: B6's "blank issue
form" traced back to origin/main never having received fork work since
2025-09-09, not a prefill bug — fixed via PR #19 (cline-dev -> main, 6276
files), which also surfaced and fixed real CI drift hidden by 5 days of
unpushed commits. B2 and B5's remaining GitHub actions are done; B6 needs a
fresh run now that bug_report.yml actually exists on main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
First release cut since origin/main was synced (PR #19) and since Apple
signing credentials (A1) were added as GitHub secrets. v0.1.0 stays a draft
with no macOS artifact; this is a fresh build to actually produce one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…runtime require

v0.1.1's packaged app crashed to a blank black launcher screen on first
launch — the very first time a real signed macOS build was actually opened.
Renderer console: "Cannot find module 'eslint-linter-browserify'".

get-externals-modules.js treats every top-level node_modules package as
webpack-external (left as a runtime require()) unless excluded via a
!node_modules/<name> files pattern, which instead forces static bundling.
eslint-linter-browserify is a dependency of noodl-core-ui (imported by
esLintDiagnostics.ts for the code editor's live lint), not of noodl-editor
directly -- present in the hoisted root node_modules at webpack time (so it
got externalized), but electron-builder's dependency pruning for the
packaged app doesn't trace it back across the workspace boundary from
noodl-editor's own package.json, so it never reaches the packaged
node_modules.

Verified locally: the production renderer bundle now inlines the Linter
class directly, zero remaining require("eslint-linter-browserify") calls.

Bumps to v0.1.2 -- v0.1.1's draft stays as a record of the defect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ter-browserify

v0.1.2 traded one blank-launcher crash for another: "Cannot find module
'globals'" (esLintDiagnostics.ts:46, same orphaned-dependency shape as
eslint-linter-browserify -- imported directly, present in hoisted root
node_modules so webpack externalized it, but not declared in any
package.json in this monorepo so electron-builder's dependency pruning
never traces it into the packaged app).

Did a full sweep this time instead of fixing one crash at a time: every
bare-package require() remaining in both the renderer and main production
bundles, cross-checked against noodl-editor's actual dependency graph
(direct + transitive through @noodl/platform-electron -> platform-node,
@octokit/*, react-dom, @noodl/git). Found one more of the same shape --
lodash, used directly in projectmerger.nodeequals.js/projectmerger.diff.ts
but declared nowhere in this monorepo -- and excluded it too, since it's
likely subject to the same pruning gap even though it wasn't the one
actually observed crashing yet.

Verified: renderer bundle has zero bare requires for
eslint-linter-browserify/globals/lodash; main bundle's full require list is
entirely Node builtins or already-covered direct dependencies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The README still described a different product: OpenNoodl branding, a
"Building from source" section pointing at "the original Noodl documentation
below" followed by a whole second copy of Noodl's own README (dead
noodl.net/noodlapp links, a Discord invite that was never ours), and a
backend section describing the old Parse-based OpenNoodl Hosting / Noodl
Cloud Services setup rather than nodegx-backend's actual workflow + cloud
function model this repo ships today.

Rewritten from verified current state: real Discord (confirmed live and
staffed), the docs site that just went live at
the-low-code-foundation.github.io/NodeGX/, an honest per-platform signing
status (macOS signed/notarized as of v0.1.3, Windows not yet, Linux never),
and the actual backend model from BACKEND-AUTHORING-MODEL.md. Dropped the
AUR install line (points at a build several years behind current source)
and the Fibery community link (Discord is what's actually wired into the
app's own Help Center).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s the sibling assertion

PR #20 CI (2026-08-07, seed 82518): "Project import and export unit tests
re-keys imported node ids while reusing the target component id" failed on
one of two parallel runs, same commit, different random seed -- confirmed
flaky, not a regression.

This exact test's own comments already document three prior rounds of
diagnosing identical order-dependent-identity flakiness through
NodeLibrary.instance (a singleton shared across the whole suite), each
resolved by asserting a stable property instead of raw object identity --
e.g. `instanceNode.type.name === '/comp1'` a few lines above this one,
explicitly captioned "asserted, unlike identity, because it is
order-independent". That caption turned out to be wrong for THIS assertion
too: `NodeLibrary.instance.typeCache.get('/comp1')` toBe(importedComp1) is
the last remaining raw-identity check in the spec, and seed 82518 is the
seed that proves it isn't order-independent either. Same fix as its
siblings: assert the name resolves to a real component, not that it's the
literal same object.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…projects depend on

Phase 18 (Code Export v2) starts here. `packages/nodegx-core` is the reactive
library that lets generated React read as an ordinary app rather than as a wall of
useEffect chains simulating the graph's event flow.

Two steps came before any code, and both changed the result.

**The behaviour contract** (`CONTRACT.md`) is read from `packages/noodl-runtime`,
clause by clause, each cited to the source line it came from. Three things the task
doc asserted turned out to be false about the runtime it was describing:

  - there is no per-frame de-duplication of signals. Ports are edge-triggered and a
    pulse carries its own reset, so two pulses in one frame fire the handler twice.
    Implementing the doc as written would have silently dropped events.
  - the runtime does not coalesce queued values — every send is delivered.
  - it does not skip an identical write; `setInputValue` has no equality check.

The last two are the conventional default for a reactive library, which is why
`Value.set` deliberately has none.

**The target output** (`EXP-001-TARGET-OUTPUT.md`) hand-writes the exported code we
want for four real components from `project-examples/agent-chat` before designing an
API for it. Its most useful finding is that most exported code should not mention
the library at all: a component input is a prop, and a signal whose downstream is one
side effect is a handler. The API is smaller because that exercise happened first.

What landed: value, derived (lazy, memoised, auto-tracked), signal, store,
collection, event bus, effect, a pluggable turn scheduler, and React hooks over
useSyncExternalStore.

Verification:
  - 69 tests in-package, including one per contract clause, named by clause
  - 8 parity tests in `noodl-runtime/test/nodegx-core-parity.test.ts` that run the
    same scenario through the *actual interpreter* and compare event sequences.
    They live there because that is where the interpreter already runs headlessly.
  - 2.8 KB gzipped against the 8 KB design budget

Gates: `typecheck:nodegx-core` and `size:nodegx-core` added to PR CI, and the
package added to `test:packages`. It typechecks under `strict`, unlike the root
program, because its types are the API surface every exported project sees.

MIT, not the editor's GPL: this ships inside the code a user exported, and a
copyleft dependency there would make the ownership this feature exists to give
conditional.

Left open and human-gated: ownership of the `@nodegx` npm scope and the first
publish. Not covered: re-render-on-change through a real DOM commit, since the
repository carries no jest-environment-jsdom by an existing documented decision —
EXP-002's first generated app is where that gets looked at.

Phase 7 is stamped superseded, as its successor's PROGRESS asked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… property

`borderRadius`, `borderWidth` and the four box-shadow length ports coerced
their value with `value.value === undefined ? Number(value) + 'px' : …`.
`Number('var(--radius-lg)')` is `NaN`, so the property was set to the literal
string `'NaNpx'` — which the CSSOM rejects on assignment, dropping the
declaration with no error anywhere.

The asymmetry is what kept it invisible: `fontSize` and `color` are declared
with the same port type but reach the style object declaratively, so tokens
always worked there. AI-authored pages therefore rendered with a correct type
hierarchy and no rounded corners or borders at all.

Adds `cssLength()`: `{value, unit}` and bare numbers behave exactly as before,
and anything else is passed through for the browser to resolve. It returns
`undefined` rather than a string for an unset value, so the `||` fallback to
the all-corners/all-edges value works again — `'NaNpx'` used to be truthy and
win it.

Verified live against the running project: `puppiesListGroup` went from
`border-radius: 0px` / `border-width: 3px` (the CSS fallback, not the declared
value) to `12px` / `1px`, matching `--radius-lg` and `--border-1`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… port

The element configs are authored as CSS — they also drive the editor's own
variant rendering — but `get_style_vocabulary` hands their `baseStyles` to an
agent as "copy these parameters onto the node". Two of them are not ports:
`boxShadow` (the runtime declares `boxShadowEnabled` plus five components) and
the `padding` shorthand (one port per side). An unknown parameter is dropped at
apply with a `warning`, and `validate.ts` is `ok: errors.length === 0`, so
nothing blocked.

That is how the authored page ended up setting `boxShadowColor` to
`var(--shadow-sm)` — a whole shadow value on a colour port: the agent improvised
around a documented parameter that does not exist.

Expands only the two shorthands the configs actually use, and passes everything
else through rather than filtering against a hardcoded port list that would go
stale as soon as a port is added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An AI build that agreed a Puppy object with name/photo/breed/age/description
provisioned `CREATE TABLE "Puppy" (objectId, createdAt, updatedAt, ACL)` — no
user columns at all — and then authored pages wiring `prop-name`, `prop-breed`
and `prop-age` at ports that could not exist. Nothing errored.

Two causes, and fixing either alone still yields zero columns:

1. `provisionFromScope` took collections from `backend.collections` when the
   conversation named them and otherwise from `scope.objects` — passing
   `fields: undefined` for the second. The two shapes differ:
   `ScopeCollection.fields` is already `{name, type?}`, `ScopeObject.fields` is
   prose ("title (text)"). Nobody wrote the translation, so the common case —
   objects agreed, no explicit collections — lost every field. Adds
   `parseFieldPhrase` for the parenthesised, dashed and colon forms, keeping a
   hyphenated name intact.

2. `toColumn` dropped any field whose type hint did not map, which is every
   bare name — and ARCHITECTURE.md recorded all five Puppy fields as bare
   names. Now defaults to `String`, reading the field name as a last hint.

The dropping was justified by "the backend infers the type from the first
record written". That is true of the storage and false of everything above it:
the column is what gives a Record node its `prop-*` ports, so a dropped column
leaves the authored UI with nothing to bind to and no way to write that first
record. `String` is always a legal column type, so the schema manager cannot
refuse it.

Two existing tests pinned the old behaviour as correct and are updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… is an error

`Puppy test 3`'s AI-authored Admin page had `page-4` (the `Page` node) with
**zero** children, while a Group labelled "Page Root" carried all twenty content
nodes as a second, parallel visual root. A page renders only what is parented to
its Page node, so the whole page rendered blank.

`validate_project` reported 0 errors and 0 warnings on it, so the plan applied
and the build reported success. The only thing that ever noticed was the running
runtime, as a soft per-node warning in a preview nobody had to open.

Deliberately narrow, because `orphanedNode` already documents why "a visual node
with no parent" cannot be flagged on its own — legitimate canvas roots have no
parent, and logic nodes are parentless by nature. This fires only on the
unambiguous shape: the Page has no children AND a parentless node with children
exists elsewhere in the component. An empty page stays an empty page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The runtime emitted no `box-sizing` at all, so the browser default
`content-box` applied and a node with `width: 100%` AND padding overflowed its
parent by exactly the padding — measured on an authored card: content box 340px,
scrollWidth 380px, with the text clipped by the parent's `clip`.

There was no way to author a padded full-width container. `alignItems` has no
`stretch` (the validator rejects it and offers only flex-start/flex-end/center),
so the only workaround was hand-computing pixel widths against the parent —
340 minus 2×20 padding — which neither a person nor an agent should have to do,
and which silently breaks the moment the parent's width changes.

Declared through `addInputCss` rather than a hand-written setter, so it is a
pass-through and cannot acquire the coercion bug that `borderRadius` had.

This is a real behaviour change: a layout tuned around the old overflow will
shift, and can opt back out per node with Box Sizing = "Content only". Taken
deliberately — `border-box` is what "width" means to anyone who is not a CSS
historian, and legacy layout compatibility is not a constraint for this fork.

Measured after: the inquiry card is 560px as authored rather than 626px, and no
element in the page has scrollWidth > clientWidth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… have found it missing

An AI-authored page rendered every word at weight 400 — correct type scale,
no type hierarchy, which is most of why such a page reads as undesigned. The
cause was not the model: the project carried 18 `fontWeight` parameters across
five components, and no node in the runtime has ever had a port to consume one.

The design system had assumed the port all along. Nine `--font-*` weight tokens
ship and are injected into every page, POL-006 added four Inter faces precisely
to serve them, every Text/Button/Input variant in `ElementConfigs` specifies a
`fontWeight`, and `StyleVocabulary.toPortParameters` hands those variants to an
authoring model as the worked example. Only the port was absent, so the
documented styling was dropped in silence. It is typed like `lineHeight` — a
unitless number with an `Auto` escape — rather than as an enum of 100…900,
because `var(--font-semibold)` is the documented way to say it here and an enum
port rejects a token.

Nothing reported any of this, and the second half of the commit is why.
`checkParameterValues` skipped every node whose type declares dynamic ports.
That is 88 of 175 types, and for 20 of them — `Text`, `Group`, `Image`,
`Button`, `Text Input`, the whole visual vocabulary a page is drawn from — the
dynamism is `declared-port-groups`, whose members the catalog enumerates in
full. So the one rule that catches a parameter naming no port was switched off
for exactly the nodes that matter. It now keys on `hasRuntimeDynamicPorts`, the
distinction `CatalogIndex` already drew and nothing used.

The same port groups carry a `condition`, and nothing outside the property panel
read it, so a parameter aimed at a switched-off port was accepted in silence too
— `Image`'s `objectFit` under `sizeMode = explicit` being the case that rendered
800px-tall photos through a clean report. `InactiveConditionalParameter` reports
those. It deliberately does not suppress the value rules the way
`ConnectionOnlyParameter` does: `Image { width: 228 }` is wrong twice over, and
a repair round told only half produces a second broken image.

`validation/portConditions.ts` evaluates the condition rather than importing the
canonical evaluator, because `validation/` reaches nothing but itself and
`noodl-types` — it runs in the editor, the MCP server and a CLI. The two are not
mirror images by design: the canonical one must answer for every input, this one
may abstain, and it does on `#js` and `{{portname}}` forms. `portConditions.test.ts`
runs both halves over every distinct condition string in the shipped catalog and
asserts this one never calls a port off that the canonical one would show.

Measured on `Puppy test 3`, which validated with zero diagnostics: 43 findings,
19 phantom parameters and 24 inactive conditional ones, including the
`textinput.width` that made a 100%-wide input render at 170px. Rebuilding the
runtime against the unchanged project turns its headings from 400 into 700/600.

Two true positives fell out of fixtures rather than code: the legacy corpus
carries 12 classes of dropped parameter name (`Image.image` is now `src`,
`Group.fill` is `backgroundColor`, `Text.textAlign` is `textAlignX`), and a
page-registration fixture authored `Group { text }`, which Group has no port for.

Gates: editor 953/69, viewer 883/65, noodl-mcp 196/18, catalog:check,
typecheck:viewer, typecheck:editor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…untime

Serves a v2 project straight from disk to the built viewer, with no editor
involved, so a page can be rendered and *measured* — which is how the last three
sessions' layout defects were actually found. `scrollWidth > clientWidth` found a
padded container overflowing its parent, `offsetWidth === 170` found a
`width: 100%` that a `sizeMode` default had voided, and a uniform
`getComputedStyle().fontWeight === 400` across every node found a port that did
not exist. None of the three is visible in the graph, and an agent authoring
blind can find none of them.

It bypasses the editor on purpose. The editor holds its project in memory and
pushes that to the viewer over a WebSocket — there is no JSON endpoint — so a
write to disk is invisible to a running editor until the project is reloaded,
and quitting it can flush the stale copy back over the change.

The export contract in the header is the part worth keeping: seven things
`renderDeployed` requires that the on-disk format does not supply, each one
reverse-engineered from a blank page. Design tokens are the subtlest — they are
stamped in by the editor or the html-processor and never by the bundle, so this
takes them from a running editor when there is one and otherwise parses the
shipped defaults out of `DefaultTokens.ts`. Rendering with no tokens at all is
not a smaller problem than rendering with the wrong ones; it just fails quietly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`get_style_vocabulary` is the one document an authoring model gets about this
design system, and its `variantStyles`/`sizeStyles` are the worked examples it
inlines close to verbatim. A property in there with no port behind it is not a
documentation slip — it is styling the model will confidently emit, the runtime
will silently drop, and the validator will pass.

This has now happened three times, each found by looking at a screenshot rather
than by any check. `boxShadow` and the `padding` shorthand went in f62877b
after an authored page came out with no cards; that pass left `fontWeight`, the
biggest one, which is why every word of a page rendered at 400; and writing this
gate immediately surfaced eleven more.

The eleven are recorded rather than repaired, because the repair is a
design-system decision. All of them are `Text` asked to behave like a box —
`code` wants a background, padding and a radius, `blockquote` wants a left
border — and `Text` deliberately is not one here; a boxed piece of text is a
`Text` inside a `Group`. `fontStyle` is the odd one out and the same shape of
hole `fontWeight` was: a typography axis with no port at all. Either `Text`
grows those ports or the variants stop claiming them, and both are Richard's
call.

What the list does today is stop a fourth instance arriving unnoticed. A second
assertion keeps the list from rotting into a lie: add one of these ports and the
entry must come out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ned styles left

Closes the three gaps the vocabulary gate surfaced in 4ae3b49, the way Richard
called them.

`fontStyle` becomes a port. It was the same hole `fontWeight` was — a typography
axis the design system referenced and the runtime could not express, so the
`blockquote` variant has specified `italic` since it was written and nothing
could ever apply it. An enum rather than a token-bearing number, because unlike
weight there are no `--font-style-*` tokens it would have to accept.

The other ten orphans were `Text` asked to behave like a box, and `Text`
deliberately is not one here — a boxed piece of text is a `Text` inside a
`Group`. So the variants stop claiming what the node cannot do: `code` keeps the
mono face and drops the tint, padding and radius; `blockquote` keeps the italic
and the muted colour and drops the rule and its indent; the button's `link`
variant drops a `textDecoration` that never rendered, hover state included. Each
carries a comment naming the `Group` wrapper that supplies the rest, so the
recipe is written down where the variant used to lie about it. `KNOWN_ORPHANS`
is now empty and the gate is what keeps it there.

`alignItems` gains `Stretch`. Without it a row of cards could not be made
equal-height at all: each sized to its own content, and the measured row was
`401, 378, 401` — one card sitting 23px proud of its neighbours because its
description ran a line shorter. The only workaround was a hand-computed fixed
height, which breaks on the next content change. Verified on a copy of
`Puppy test 3` with the one parameter changed: both rows render `401, 401, 401`.
`Layout.size` only writes `alignSelf` for a child that declares `alignX`/`alignY`,
so a card that declares neither inherits the stretch — which is why this works
without touching the layout engine.

Gates: editor 958/70, viewer 883/65, noodl-mcp 196/18, catalog:check,
typecheck:viewer, typecheck:editor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Typography that an authoring model writes now reaches the page. 0.1.3 had no
`fontWeight` port and no `fontStyle` port at all, so every word of an
AI-authored page rendered at weight 400 however the model styled it, and the
check that should have said so was switched off for the whole visual vocabulary.

- a Font Weight port, and Font Style alongside it
- `Stretch` on Align Items, so a row of cards can be equal-height
- the unknown-parameter rule now runs on Text/Group/Image/Button/Text Input
- a new warning when a parameter sets a port a sibling has switched off
- the style vocabulary can no longer teach a style with no port behind it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…wise

Both files still described the pre-certificate world, and that cost something
real: they were read while cutting v0.1.4 and produced the confident, wrong
advice to paste INSTALLING-UNSIGNED-BUILDS.md into the release notes — telling
users to bypass Gatekeeper on a build that is signed and notarised.

The five Apple secrets went in on 2026-08-07. Both v0.1.3 and v0.1.4 signed and
notarised first time, with none of the entitlement trouble the "known
limitations" section anticipated. The consequence nobody had written down is the
one that matters most: **macOS auto-update was blocked on exactly this.**
Squirrel.Mac will not install an update to an unsigned app, so the in-app
updater — built and wired since REV-007 — had never been able to do anything.
RELEASE-PROCESS.md said so in one place, filed under a future limitation, while
its own status banner said only that Gatekeeper "will warn".

Also corrected:

- The multi-arch mac feed is no longer a future problem. The `merge mac update
  feed` job rebuilds `latest-mac.yml` from both legs and v0.1.4's published feed
  carries all four mac files under one version.
- "There is no `latest-linux.yml`, and there is not meant to be" is wrong on the
  first half. electron-builder emits one and v0.1.4 shipped it. It is inert —
  the early return in `autoupdater.js` is what makes Linux install-only — but a
  published feed nothing reads looks like a promise the app does not keep.

Two things deliberately NOT claimed as done. **Windows signing is unconfirmed:**
`WIN_CSC_LINK` is unset, yet electron-builder logged `signing with signtool.exe`
and raised no error, which is ambiguous rather than reassuring, and no artifact
has been past SmartScreen on a clean machine. And **the upgrade path has still
never been exercised** — the blocker is gone, but "0.1.3 installs 0.1.4 by
itself" has not been observed, and it cannot be tested from CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit listed "the upgrade path has never been exercised" as an
open limitation. It was exercised an hour later: 0.1.3 → 0.1.4, unattended, on
macOS arm64. Feed read, 169MB zip downloaded, Squirrel swapped the bundle, and
`codesign --verify --deep --strict` still passes on the replaced app.

The finding worth writing down is not that it works — it is that it presents as
a failure to the person it is working for. The update applies on **quit**
(`autoInstallOnAppQuit` defaults true, `launchAfterInstallation => false`), and
`autoupdater.js` messages the renderer only on `update-downloaded`: nothing on
`update-available`, no `download-progress`. So the app sits silent through a
~90-second download, and restarting inside that window shows nothing at all.
That is exactly how the first real run was experienced — reported as "doesn't
show anything about updating" while ShipIt was mid-install. Both unused events
are available; wiring them is the fix, recorded here rather than done blind.

Also records the three places evidence lives, because there is no app-level
update log and the instinct is to look for one: `ShipIt_stderr.log` (which says
`Aborting update attempt because there are 1 running instances` and then
`Installation completed successfully`), the updater cache whose zip size can be
checked against `latest-mac.yml`, and the installed bundle's Info.plist.

And a warning about a red herring that cost time: `releaseType: draft` in the
shipped `app-update.yml` is the publish-side setting copied into the read-side
config and looks like a smoking gun. `GitHubProvider` never reads it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e progress

An update used to download 169MB and replace the app on quit with no prompt, no
progress and no consent. The first time it ran for real it was reported as "it
doesn't show anything about updating" — by someone who was being silently
updated as they said it. That is the whole brief: an update should be something
a user agrees to, having seen what is in it.

`autoDownload` and `autoInstallOnAppQuit` are now both off. Nothing leaves the
network and nothing is replaced until someone clicks. The dialog leads with the
release notes, because an update can carry breaking changes and "a new version
has been downloaded" — all the old confirm box could say, arriving after the
fact — is not enough to consent to.

**Choosing a version.** electron-updater has no notion of installing a
*particular* release; it installs whatever the feed says is newest. But every
release publishes its own feed file beside its artifacts, describing that
version — so a `generic` provider aimed at one tag's download path is a feed
with exactly one version in it. That, plus `allowDowngrade`, is the picker.
Pinning happens only when the choice is not the newest, because the github
provider is the one that understands channels and pre-releases.

**What may be offered is filtered on the feed asset, not the tag.** `v0.1.2` is
a real published release with no `latest-mac.yml` — it predates signing — so a
list built from tags would offer an install whose only outcome is a 404. Drafts
go for the same reason. Checked against the live API: the picker offers 0.1.4
and 0.1.3, and correctly drops the stray `release` tag.

**The launcher had no update surface at all.** `BaseWindow`, which owned the old
popup, wraps only `EditorPage` — so an update could be offered only to someone
already inside a project, the one place a restart costs most. `UpdateManager`
mounts in both, and a progress pill outlives the dialog: the download is not
dismissible even though the dialog is, and a transfer running with nothing on
screen is exactly how someone quits mid-update believing the app is idle.

Also: `Text` gains `Danger`, `Notice` and `Success`. The stylesheet has carried
`is-type-danger` all along with no enum member able to select it, so a red line
of text meant inlining a colour — which is how a palette stops being one file.

Removes `showAutoUpdatePopup` and the `window._hasNewAutoUpdateAvailable` global
it set, which nothing ever read.

Not verified: the dialog has not been seen rendered. It appears only when an
update is available and this machine is on the newest build; there is no React
test environment in this package (jest runs in `node`, no RTL). The logic is
pinned by 18 main-process tests including the consent guarantee; the appearance
is not.

Gates: editor 967/70, core-ui 179/14, typecheck:editor, typecheck:viewer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Updates stop happening to you and start being something you agree to.

- nothing downloads or installs without a click
- the dialog leads with the release notes, so a breaking change is visible
  before you consent to it rather than after
- a version dropdown, so being several behind does not force you to the newest
- visible download progress in both the launcher and the editor, so nobody
  quits halfway believing the app is idle
- the launcher can offer an update at all, which it never could before

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No functional change. Cut so 0.1.5's new update dialog has something to offer:
a release cannot demonstrate its own updater, because the code doing the
updating is the *installed* version's, not the new one's. 0.1.4 delivered 0.1.5
the old silent way for exactly that reason.

With this published, an editor on 0.1.5 sees the real thing — the consent
dialog, the release notes, visible progress, and a version dropdown with four
installable entries rather than a list of one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant