Conversation
enable-unsafe-webgpu was applied on any X11 session (!isWayland), leaving WebGPU silently ON for X11 users who had the enableWebGPU toggle OFF — contradicting the setting. Figma probes navigator.gpu (exposed by enable-unsafe-webgpu) to switch on its Shader/Halftone/ Noise effects, so the flag belongs behind the toggle alongside the shaders. Now gate it on shadersActive (X11 + enableWebGPU) with the existing userForcesVulkan escape hatch preserved. Only the X11 + toggle-off quadrant changes; X11/Wayland toggle-on and Wayland toggle-off are unaffected. The change is scoped to WebGPU only — ignore-gpu-blocklist and enable-webgl stay on X11, so the normal WebGL canvas keeps its hardware acceleration; a toggle-off X11 user loses only the shader effects. Update the truth-table tests to lock all four quadrants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QWRSUEUKBsLy4paZR4noBb
"Export video" calls window.open("https://www.figma.com/export/<fileKey>?fuid=…").
/export/ was missing from isFigmaRunUrl's validPaths, so the URL fell through to
shell.openExternal and landed in the system browser — where the render queue is
always empty, because it lives in the app's session. Logging in there does not
help, as reported in #41.
- add /^\/export\// to validPaths so the queue opens as an in-app tab
- give export tabs their own dedup prefix (export:<key>), so a repeated
"Export video" click reuses the queue tab and cannot collide with the
design tab for the same file
- log every window.open with url/frameName/disposition/features — Tab had no
logging at all here, which is why this took a live capture to find
- route only http(s) to shell.openExternal; other schemes were passed through
as a silent no-op and are now logged as blocked
Verified against a live capture: the export URL above, and the Linear plugin's
OAuth (response_type=stringauth, redirect_uri=figma-plugin.linear.app/callback),
which completes in the browser and returns through Linear's own callback.
Refs #41
NixOS packaging
The flake read its version from package.json but carried hard-coded sha256 hashes, so every `perl scripts/bump_version.pl` left it naming a release whose binaries it could not hash — `nix build` then failed on a hash mismatch until someone recomputed both by hand. The PR shipped with a "Chore: update hashes when release binaries change" comment; this replaces the comment with the mechanism. - flake.nix: version and both hashes live in one `release` block, so they are rewritten as a unit and cannot drift - scripts/update_flake_release.py: rewrites that block, converting sha256sum hex digests to the SRI form Nix expects; fails loudly if the layout moved - release.yml: new `flake` job hashes both release zips after the release is published and commits the pinned flake to staging (dev is protected, so the flake there trails by one release) - README: note that the flake installs prebuilt binaries and how to pin a tag - CLAUDE.md, build skill: document that flake.nix is CI-owned, never bumped by hand Not verified with `nix build` — no Nix on this machine. Follow-up to #42
Nix resolves `github:arximus88/figma-linux-next` from the default branch, which is dev. Committing the release hashes only to staging meant NixOS installed whatever dev still named — always the previous release, since staging reaches dev with the next release PR. The flake job now checks out dev with RELEASE_PAT (fine-grained, Contents:write on this repo only) and pushes there first, then mirrors the same change to staging. The default GITHUB_TOKEN cannot do this: it is refused by the branch protection on dev. The job still runs last and depends on `release`, so a rejected push leaves the release, its binaries and both AUR packages already published — the only casualty is the flake update, and the failure is visible in the run. Token expiry (2027-08-05) is recorded in CLAUDE.md and the build skill, because a 403 on that push is the sole symptom and would otherwise be a blind diagnosis.
The gemini-cli copy under .agent was byte-identical to the global ~/.claude/skills/gemini-cli. The .gemini skills duplicated capabilities already available (frontend-design, the Figma MCP workflows, generic refactoring smells) or contradicted this project: electron-app-planning prescribes raw ipcMain.handle and electron-updater, neither of which applies here. .gemini/settings.json listed the same MCP servers as .mcp.json plus figma-remote. One item survives as a CLAUDE.md note instead of a file: `bunx @sveltejs/mcp svelte-autofixer <file>` catches Svelte 5 rune antipatterns that svelte-check misses, which matters because Biome does not lint .svelte. The gitignore "Agents" block matched only .gemini/settings.*, so the skills stayed tracked despite it; widened to .gemini and dropped the duplicate .agent/skills entry.
… Nix section The Nix material was already current — the skill was written alongside 358ada0/75b23e4. Verifying it line by line against release.yml, update_flake_release.py and flake.nix surfaced five other divergences: - The release checklist pushed the tag together with the branch, ahead of the PR steps, contradicting the rule stated at the top of the same file. Following it literally would publish a release before dev was updated. - push_aur_dev_git.yml was listed as an existing workflow; 3c3a762 deleted it. - RELEASE_PAT was described in prose but missing from the secrets table, so its 2027-08-05 expiry was not where someone would look for it. - The aur/aur-bin/flake pre-release guard from e011888 was undocumented: an -rc tag produces a GitHub Release and no AUR or flake update. - Key Files listed no flake file at all — neither flake.nix nor scripts/update_flake_release.py.
CLAUDE.md:
- Critical Files pointed at src/utils/Render/frameConfig.ts and
frameStyles.ts; neither exists. The icon/component config is
frameTheme.ts and the frames themselves are Svelte components under
src/renderer/Panel/frames/.
- Utils/Render/defaultSettings.ts was called the authoritative settings
schema. It is a four-line re-export whose own comment calls itself a
placeholder; the values live in Utils/Common/defaultSettings.ts as
BASE_DEFAULT_SETTINGS. Documented all three same-named files so
"add a new setting" lands in the right one.
- The path-alias example imported a symbol named `defaultSettings`,
which nothing exports.
- FrameStyle was listed as three values; the union in
src/types/Common/index.d.ts has four (kde was missing) and the
default is gnome.
- ci.yml was described as PRs to dev; it runs on push and PR to both
staging and dev.
README.md:
- The AppImage snippet globbed figma-linux-next-*.AppImage with a dash.
electron-builder names artifacts ${name}_${version}_${platform}_${arch},
so the glob matched nothing.
- The NixOS section set programs.figma-linux-next.enable without
importing nixosModules.default, which fails evaluation with an
undefined option.
Both AUR packages publish license=('MIT') while the project is
GPL-2.0-only — LICENSE is GNU GPL v2, and package.json, src/package.json
and flake.nix all say so. Verified against the live AUR RPC:
figma-linux-next 0.15.0-1 and figma-linux-next-bin both report
License: ["MIT"].
The line drifted because the aur and aur-bin jobs rewrite pkgver, pkgrel
and sha256sums and nothing else, so whatever license the AUR repo was
seeded with survived every release. Deriving it from package.json means
the two can no longer disagree; the value there is already an exact SPDX
identifier, so it drops into the PKGBUILD verbatim.
python3 is used rather than jq because the archlinux container installs
python and not jq.
Verified: actionlint clean, YAML parses, and the sed produces
license=('GPL-2.0-only') against a real PKGBUILD.
A clone under /home/arx/aur/ had drifted five releases behind live AUR (0.13.1 vs 0.15.0) while still looking like a working copy. Pushing it would have rolled both packages back, and CI would not have caught it — each run clones whatever AUR currently holds and edits that in place. Also records which fields CI rewrites (now including license) versus the ones that only change by hand, and that AUR maintenance takes down the SSH endpoint alone: push fails while the web UI and HTTPS git stay up.
The project sat on 42.0.1 because 42.3.0 broke `/app_auth/redeem` — Figma rejected the request headers from that Chromium roll and returned login HTML instead of Set-Cookie, killing first-login and add-account. Verified against the bundled 43.3.0 binary (Chromium 150.0.7871.212), which is what electron-builder ships: first login on clean storage completes, the figma:// redeem redirect lands, and the session survives restarts. Image paste and frameless window decorations also check out, covering the two behaviour changes in 43 that touch us — sRGB normalisation in nativeImage.toBitmap() and rounded corners on Linux CSD. Chromium 148 -> 150 also brings three months of security fixes, a faster boot (startup snapshot for main, bytecode-cached preloads) and a newer WebGPU stack. CLAUDE.md now records how to run the OAuth test correctly: use the bundled binary rather than a distro Electron, carry mimeapps.list into the isolated config, and shadow the installed .desktop so the redirect comes back.
Electron 43, NixOS packaging, Figma Motion export fix
Both signatures failed on the panel root: 1168px recorded, 1200px rendered. The gap is not a regression — the value does not reproduce anywhere. Checked against Electron 42.8.0 and 43.3.0, under xvfb and on a live Wayland session, and at e761686, the very commit that wrote the file: every combination renders 1200px, which is exactly WINDOW_DEFAULT_OPTIONS.width. That default has not changed in years, so the recorded 1168px came from an environment we no longer have — most likely an Electron 42.x patch that aligned the frameless client area with the requested window size. The refreshed files differ by four lines, all of them viewport-derived widths (root 1168->1200, tab strip 911->943). Structure, colours and spacing are untouched, so the oracle keeps its value. Verified passing both under xvfb and on a real display after the update.
Found while exercising the Zenity provider by hand against the restart prompt. Three defects, all in both showMessageBox and showMessageBoxSync: --ellipsize was passed on every dialog. It exists to keep the window small on long text, and it did that by cutting the sentence off: the restart prompt read "Figma needs to be restarted to app…", so the user never saw which setting required the restart. Replaced with --width=460, which bounds the window and wraps instead. --text was pushed only when `detail` was set, so a message-only dialog rendered with an empty body — the message was dropped on the floor. It is now always passed, with detail appended when present. A zenity --question labels its reject button "No" while the native provider says "Cancel", so the same prompt read differently depending on the provider. The cancel label now defaults to "Cancel". Verified by hand: the restart prompt shows the full detail line and Restart / Cancel buttons under the Zenity provider.
Opening Figma Motion's video-export queue left it stuck on "Your rendering queue is empty" even though the render was queued and progressing. Switching tabs away and back was the only cure. A CDP session on the stuck page ruled out every visible explanation. It is indistinguishable from a working one: document.visibilityState is "visible" and no visibilitychange ever fires, innerWidth/innerHeight are identical before and after the switch, requestAnimationFrame runs at 60fps, and the preload bridge is installed. Reloading the tab, resizing the window, maximising it and refocusing it all fail; only removeChildView followed by addChildView works. Doing that pair programmatically right after load did nothing either — Chromium collapses a detach/attach that happens in one synchronous block, so the layer is never rebuilt. What the manual switch actually provides is order: the page finishes loading while detached, and is attached afterwards. So the tab is now held back instead of nudged. It loads in the background at 0x0 like a warm tab and is attached once did-finish-load arrives. The tab strip is unaffected — addTab already sent didTabAdd — only the view is deferred, with a timeout so a failed load cannot leave the tab hidden. isExportQueueUrl keeps the deferral scoped to the queue; a design file with "export" in its slug is not affected, which the test covers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release PR for v0.16.0. Tag is held locally and gets pushed only after this merges.
Electron 43.3.0 (Chromium 150)
The project was pinned to 42.0.1 because 42.3.0 broke
/app_auth/redeem. Verified against the bundled binary — the one electron-builder ships, not a distro rebuild: first login on clean storage completes, the session survives restarts, image paste is unaffected by 43's sRGB normalisation intoBitmap(), and window decorations are unchanged despite the rounded-corners default on Linux CSD.NixOS support
Flake from @iamcalledrob (#42), reworked so version and both platform hashes live in one
releaseblock that CI rewrites as a unit after each release. The newflakejob pushes todevfirst — where Nix resolves the input from — and mirrors tostaging.Fixes found during manual testing of this branch
did-finish-loadand attached after, which is the ordering the manual switch was really providing.--ellipsizecut prompts off mid-sentence,--textwas dropped entirely for dialogs without a detail line, and the cancel button read "No" where the native provider says "Cancel".package.jsoneach release — it was hardcodedMITwhile the project isGPL-2.0-only.Test baselines
frame-signaturefailed on both styles: 1168px recorded, 1200px rendered. Not a regression — the value reproduces nowhere, including at the commit that wrote the file, under Electron 42.8 and 43.3, under xvfb and on a live display. 1200px is exactlyWINDOW_DEFAULT_OPTIONS.width. Refreshed; the diff is four viewport-derived widths, structure untouched.Checks
bun test214 pass ·bun run test:e2e20 pass ·tsc --noEmitclean ·biome checkclean ·svelte-check0 errorsManually exercised on a live session: login, image paste, both frame styles, WebGPU toggle, fonts, warm tab, export queue, Zenity and native dialogs.