Skip to content

feat: plugin v2 — Svelte 5 rewrite, bulk operations, and a reorganised UI - #106

Merged
bdshadow merged 255 commits into
mainfrom
zuzka/v2-ux-perf
Sep 17, 2026
Merged

bdshadow merged 255 commits into
mainfrom
zuzka/v2-ux-perf

Conversation

@ZuzanaOdstrcilova

Copy link
Copy Markdown
Collaborator

This replaces the Preact plugin on main with the v2 rewrite that is already published to the Figma Community (live since 8 September). Publishing it built the shipped package from 26843a1, the head of this branch, so main is currently behind what users are running.

It carries Nico's Svelte 5 rewrite (#101) plus the work that followed it: 254 commits, 53 from @eweren and 200 on top.

What changes

The UI is rebuilt in Svelte 5 with Tailwind and Vite, replacing Preact and react-query. Settings are reorganised into three tabs (Project, Strings & Keys, Upload options), and Push/Pull are renamed to Upload/Download throughout.

New: bulk operations — generate key names, auto-connect by exact match, connect to one key, set namespace, and disconnect, all across a whole selection.

Rebuilt: plurals and variables now render a sample on the canvas, page copies detect when they fall out of date and offer a rebuild, and the strings list gains search and filters.

Compatibility

Storage keys are unchanged — tolgee_config in clientStorage and document plugin data, tolgee_info per node — so existing key connections, the connected project and the API key all load without migration.

writeGlobalSettings deliberately persists a JSON string rather than an object, because the published Preact reader does an unguarded JSON.parse at startup. That keeps a rollback to main working instead of throwing on every document.

The manifest now sets documentAccess: "dynamic-page", which main did not.

Testing

The e2e suite runs Playwright against a real Tolgee instance in Docker (e2e/docker-compose.yml), which also covers the self-hosted path. It was green on this commit, and @eweren tested the build through before it was published.

Note on #101

This branch contains #101 in full, so merging it makes that draft redundant. Closing it in favour of this one is the intent, but that is @eweren's call.

🤖 Generated with Claude Code

eweren and others added 30 commits May 11, 2026 16:18
CreateCopy (Phase 7):
- main/handlers/createCopy.ts: clones currentPage, rewrites connected
  TextNodes with key labels (mode: keys) or per-language translations
  (mode: languages), marks cloned pages with pageCopy: true via raw
  setPluginData to avoid leaking marker into doc/global scope.
  yields every 10 nodes, emits create-copy-progress
- writeTextSafely: hasMissingFont guard, mixed-font guard,
  loadFontAsync over all ranges before mutating characters
- create-copy / create-copy-progress / create-copy-result messages
- CreateCopy.svelte: idle/fetching/creating/error stages, mode
  picker (keys vs per-language), language checklist loaded lazily
  from /v2/projects/languages, fetchAllTranslations builds the
  \$\{ns\}|\$\{key\} -> text map for the main thread
- CopyView.svelte: read-only banner showing pageCopy state and
  language, link back to settings

Canvas Annotations (Phase 8 — feature highlight):
- main/annotations/category.ts: ensureTolgeeCategory caches the id in
  figma.root pluginData, recovers via label match across sessions,
  creates a blue 'Tolgee' category if needed
- main/annotations/sync.ts: applyAnnotations / removeAnnotations /
  syncCurrentPage / clearCurrentPage. Identical-write skip avoids
  unnecessary co-editor churn. Uses findAllWithCriteria with the
  tolgee_info pluginData filter for O(connected) scans. Dev mode
  is a hard no-op (write-protected).
- main/annotations/reconcile.ts: 200ms debounced reconcile,
  coalesces ids into a Set, resolves via getNodeByIdAsync
- main/annotations/index.ts: public API plus
  isAnnotationsEnabled/setAnnotationsEnabled backed by clientStorage
  (per-user, not per-document)
- handlers/annotations.ts: re-exports from $main/annotations
- main.ts: in-memory annotationsEnabled mirror so the synchronous
  selectionchange / documentchange handlers don't pay an async cost.
  selectionchange schedules a reconcile for the current selection.
  currentpagechange triggers a full syncCurrentPage. documentchange
  filters TEXT-node pluginData/characters/name changes and schedules
  per-id reconciles. ui-ready dispatches command handling for
  toggle-annotations / refresh-annotations and runs an initial
  full sync if enabled. set-nodes-data and apply-translations also
  kick the reconciler since same-plugin pluginData writes don't
  always round-trip through documentchange.
- toggle-annotations / get-annotations-state / annotations-state
  messages

Verification: 838 svelte-check files clean, 8/8 tests pass, build
produces dist/main.js + dist/ui.html (166 kB gzip) +
dist/ui-inspect.html + dist/manifest.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 9 — inspect panel finalisation:
- ui-inspect/lib/deeplink.ts: buildKeyDeepLink + buildProjectDashboardLink
  with projectId-aware paths and graceful fallback when projectId is
  unknown. Strips trailing slashes
- ui-inspect/App.svelte: brand header (Tolgee · language · project),
  empty-states with Plug / MousePointer lucide icons, per-node cards
  with Tag / Languages icons, ExternalLink / Copy / Check action row
  with 1s "Copied!" confirmation, footer with project deep-link,
  disabled-state when apiUrl is missing
- shared/types.ts: projectId?: number on CurrentDocumentSettings
- shared/messages.ts: persist-project-id UiToMain message
- main/settings/merge.ts: projectId routed to document scope only
- main/main.ts: persist-project-id handler writes to config and
  re-emits config-changed
- SettingsSectionConnection.svelte: after successful Test Connection,
  sends persist-project-id so the inspect panel can build
  project-specific deep-links without re-validating the key

Phase 10 — tests and bundle audit:

Unit tests (87 passing, 1 skipped):
- vitest.config.ts with shared/main/ui/inspect aliases mirroring
  tsconfig paths
- 18 tests for keyFormat (5 casings, empty/special-char/missing
  placeholders)
- 6 tests for ICU (plain, empty params fast-path, named substitution,
  plural one/other, malformed fallback)
- 9 tests for settings merge (right-wins precedence, page-only,
  doc-only, global double-write, round-trip)
- 13 tests for pushDiff (unchanged/changed/new/conflicts, plural
  mismatch, configured tags, keyless, characters fallback)
- 12 tests for pullDiff (changed/missing/unchanged, disconnected
  skip, plural mismatch, namespace disambiguation) plus formatNodeText
- 8 tests for findOutermostFrame with fake-tree casts
- 13 tests for the new tolgeeFormat parser
- deeplink.test.ts placeholder, now wired

Bundle-size optimisation (550 kB raw / 166 kB gzip
→ 223 kB raw / 68 kB gzip, -59%):
- replaced @tginternal/editor with a 130-line local ICU plural parser
  (tolgeeFormat.ts) — eliminates the @codemirror/view (376 kB) and
  @codemirror/state (142 kB) transitive deps. Bonus: pullDiff now
  extracts the real plural parameter name instead of assuming "count"
- removed tailwind-merge and tailwind-variants — cn() collapses to
  clsx, button.svelte ships inline variant maps with identical
  consumer API
- rollup-plugin-visualizer as ANALYZE=1 opt-in, both vite configs
  emit dist/stats-ui.html / dist/stats-ui-inspect.html on demand

Final state: dist/main.js (16 kB), dist/ui.html (223 kB raw /
68 kB gzip), dist/ui-inspect.html (70 kB raw / 25 kB gzip),
dist/manifest.json. typecheck + svelte-check + 87 tests all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Figma's plugin iframe runs at origin "null" and chokes on
<script type="module" crossorigin> — the crossorigin handshake fails
against a null origin and the modulepreload polyfill Vite injects
also tries to resolve preload candidates against that origin. Both
of those break the UI before our code even runs, surfacing as a
Syntax Error inside Figma's own vendor bundle when it tries to
evaluate the malformed UI.

Fix: disable modulePreload in both Vite configs and add a
closeBundle plugin that strips type="module" and crossorigin from
the inline script tag in dist/ui.html and dist/ui-inspect.html.
The bundles contain no import/export statements (inlineDynamicImports
is on and singlefile produces a flat IIFE-style module), so the
classic script tag is equivalent and Figma-safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-pronged fix for the Figma loader Syntax-Error crash:

1. main.js was being emitted as an IIFE — \`"use strict";(()=>{...})();\`.
   Figma's plugin runtime hosts the bundle in QuickJS and prefers
   plain top-level statements. Switched esbuild format from \`iife\` to
   \`cjs\` so the output begins with \`"use strict"; var ... =\` instead.
   No \`require()\` calls in the bundle (everything is inlined), so cjs
   collapses to flat top-level statements that the host parses
   straightforwardly.

2. manifest.json had \`ui: { default: ..., inspect: ... }\` — those
   keys don't match Figma's schema. The schema uses \`figma\` and \`dev\`
   for per-editor UI bundles. To rule out per-mode wiring issues
   while triangulating, the manifest is temporarily reduced to the
   minimum: api, id, name, main, ui (string), editorType: ["figma"],
   networkAccess. We re-add documentAccess, capabilities, per-editor
   UI, menu, and relaunchButtons once the loader is happy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Figma's plugin sandbox runs on a QuickJS host whose syntactical
surface is ES2017. Optional chaining (?.), nullish coalescing (??),
and logical assignment operators (??=, ||=, &&=) are ES2020+ and
surface as "Unexpected token {" in jsvm-cpp.wasm with no useful
location — the parser fails at the first such operator it sees,
which in our minified bundle often lands at an object literal's
brace.

esbuild target switched from es2020 to es2017 for the main bundle
only. The UI bundle keeps es2020 because its iframe runs in
Chrome/V8, not QuickJS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix stripped both \`type=\"module\"\` and \`crossorigin\`
from the inline script tag, which loaded the UI as a classic script.
That's wrong: Vite emits the bundle as an ES module (uses
\`import.meta\` semantics, top-level module scope) so executing it as
a classic script silently fails and leaves the iframe blank.

The actual blocker was the \`crossorigin\` attribute alone — Figma's
null-origin plugin iframe can't satisfy the cross-origin handshake.
Inline \`<script type=\"module\">\` itself works fine, it just must
not carry crossorigin.

Both Vite plugins now use a single regex that drops crossorigin and
preserves type=\"module\".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that the plugin successfully boots, re-add the features we
temporarily removed while triangulating the syntax-error crash:

- editorType: ["figma", "dev"] enables the dev-mode inspect panel
- documentAccess: "dynamic-page" matches the async page-load code
  already in the main thread
- capabilities: ["inspect"] gates the dev-mode UI
- per-editor ui mapping points figma -> ui.html, dev -> ui-inspect.html
- menu adds Open / Toggle Annotations / Refresh Annotations commands
- relaunchButtons adds "Edit Tolgee key" on connected text nodes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Figma exposes the UI source as one of two globals:
- \`__html__\`: string — set when manifest.ui is a string
- \`__uiFiles__\`: object — set when manifest.ui is a per-editor object

We switched manifest.ui to the object form (\`{ figma, dev }\`) to get a
separate inspect-panel bundle, but main.ts still referenced \`__html__\`
which is undefined in that mode. Plugin crashed with
"ReferenceError: '__html__' is not defined" before figma.showUI ever ran.

main.ts now picks \`__uiFiles__.dev\` in Dev Mode and \`__uiFiles__.figma\`
in design mode, falling back to \`__html__\` for safety.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With \`documentAccess: \"dynamic-page\"\`, Figma refuses to register a
\`documentchange\` handler unless \`figma.loadAllPagesAsync()\` was called
first. That call is expensive on large files (it materializes every
page in the document) — exactly the cost \`dynamic-page\` was designed
to avoid.

We don't need \`documentchange\` for annotation reconcile: the
\`selectionchange\` reconciler catches user-visible drift, and we
already kick the reconciler explicitly after our own \`set-nodes-data\`
and \`apply-translations\` writes. Cross-plugin edits to tolgee_info
are an edge case the user can address via the \"Refresh Annotations\"
menu command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auth store was only populated when the user clicked "Test
Connection" in Settings. On a fresh plugin open, the merged config
already contains apiUrl + apiKey (loaded from clientStorage by the
main thread), but the auth store stayed cleared — so the UI showed
the "Sign in" CTA until the user re-opened Settings and saved the
already-correct values.

App.svelte now runs maybeBootstrapAuth() on init / config-changed /
page-changed: if apiUrl+apiKey are present, it silently calls
validateApiKey and populates auth.setAuth on success. A fingerprint
guard prevents re-validating the same credentials repeatedly. If the
projectId we learn back differs from what's stored on the document,
we send persist-project-id so the inspect panel's deep-links stay
project-aware without user interaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously \"Toggle Canvas Annotations\" and \"Refresh Annotations\" from
the plugin menu invoked the plugin like any other command — figma
opened the UI window, then the ui-ready handler did the toggle as a
side effect. So clicking the menu item *both* toggled annotations
*and* popped the UI, which is not what the user expects.

main.ts now branches at startup:
- If figma.command is one of the quick-action commands and we're in
  design mode, run the side effect and call figma.closePlugin(msg)
  with a notify message. No showUI / attachBus.
- Otherwise, open the UI normally.

The Dev Mode case still skips quick-actions because the dev-mode
plugin is read-only — annotation writes are disallowed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Selection-based reconcile + currentpagechange full-sync + explicit
reconcile after our own set-nodes-data / apply-translations writes
cover ~95% of drift. The Refresh button was a fallback for the
remaining edge cases (manual annotation delete on a never-selected
node, parallel cross-plugin edits) that few users will hit. Removing
it shrinks the menu surface and one less concept to learn.

If we need it later: re-add to menu, restore the cmd branch in
ui-ready, re-add the union member in shared/messages.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bundle-size optimisation in phase 10 dropped @tginternal/editor
to save ~520 kB raw. That removed the syntax-highlighted ICU editor
the old plugin shipped, leaving a plain <textarea> in StringDetails
and Connect. The user noticed the visual regression.

Strategy: keep the local 130-LOC tolgeeFormat parser for pushDiff /
pullDiff (still saves the React + CodeMirror tax there), but mount
CodeMirror 6 only where the editor actually renders.

- Reinstated runtime deps: @codemirror/{state,view,commands,lint}
  and @tginternal/editor 1.16.1. The React Editor.tsx wrapper from
  the package is unusable from Svelte, but the underlying CodeMirror
  extensions (tolgeeSyntax, TolgeeHighlight, PlaceholderPlugin) are
  framework-agnostic.
- New IcuEditor.svelte: hand-rolled CodeMirror 6 mount with history,
  drawSelection, default + history keymap, EditorState.readOnly when
  disabled, line wrapping, and an updateListener that fires onChange.
  External $effect keeps the doc in sync with the bound value when
  the prop changes from outside (e.g. route load) without
  re-emitting.
- Placeholder pill styles defined in component-local CSS scoped via
  :global() so we don't need @emotion/styled.
- StringDetails.svelte translation textarea swapped for IcuEditor.

Bundle grows from 67 kB to 188 kB gzip; the highlight is back.
svelte-check 851 files 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old plugin let users type a Tolgee key directly into each
unconnected text row in the index, with a debounced auto-save. The
rewrite had collapsed that into a card that only navigated to the
Connect view, which felt heavy for the common case of just labelling
a few texts.

NodeList now delegates to a NodeListItem component per row:
- Connected: read-only display of ns.key, click navigates to
  StringDetails for plural/translation edits.
- Unconnected: inline <input> for the key, 300ms debounced
  set-nodes-data write, with a link icon button that opens the
  full Connect view (for key search, plural toggle, namespace
  pickers).

Hydration is gated by node.id so user keystrokes don't get clobbered
when the parent re-renders. keyFormat / prefillKeyFormat from the
saved config seeds the input on first hydrate for unconnected nodes.

svelte-check 852 files 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The push / pull / createCopy / projectMeta API calls all forwarded
\`branch: cfg.branch || undefined\`. On projects without branching
that produced
\`{ code: \"feature_not_enabled_for_project\", params: [\"BRANCHING\"] }\`
400s on the translations endpoint and broke push.

- Auth store now also carries branchingEnabled / namespacesEnabled.
- App.svelte calls getProjectMeta() right after a successful
  validateApiKey and populates those flags. Fetch errors are
  treated as "both off" (safe default that prevents 400s).
- Push / Pull / CreateCopy derive \`branch\` as
  \`auth.value.branchingEnabled ? cfg.branch : undefined\` so the
  parameter only goes on the wire when the project actually
  supports it.

While here:
- screenshots.ts and projectMeta.ts no longer poke at openapi-fetch
  internals. They take \`apiUrl\` + \`apiKey\` directly. The auth
  store now caches those strings alongside the typed client. Fixes
  the "Could not determine baseUrl for screenshot upload" error.
- auth.setAuth signature gained apiUrl + apiKey; App.svelte and
  SettingsSectionConnection.svelte updated accordingly.

svelte-check 852 files 0 errors, 87 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IcuEditor css selectors used .cm-placeholder-* — that's wrong.
Tolgee's PlaceholderPlugin sets className to plain
\`placeholder-widget placeholder-\${type}\` with no \`cm-\` prefix. The
placeholders rendered as bare text, so the editor looked like the
old plain textarea even though parsing was working.

Switched selectors to .placeholder-widget / .placeholder-variable /
.placeholder-tag / .placeholder-variant / .placeholder-hash /
.placeholder-error and added pill styling for each. hash uses indigo,
error uses Figma's danger tokens, others retain their previous colors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues with the IcuEditor highlight:

1. Plural keywords (\`plural\`, \`one\`, \`other\`, \`=0\`, ...) and the hash
   placeholder \`#\` are styled by TolgeeHighlight via tag→color but
   the colors were CSS variables (\`var(--color-text)\`, \`var(--icu-fn)\`)
   that aren't reliably resolved inside CodeMirror's inline color
   styles. Switched to concrete hex colors (green for keywords, blue
   for variableName) and \`currentColor\` for plain text so it still
   inherits Figma's theme.

2. Placeholder widget tag classes are \`placeholder-tagOpen\` /
   \`placeholder-tagClose\` / \`placeholder-tagSelfClosed\` — not
   \`placeholder-tag\`. There is no \`placeholder-variant\` either;
   plural variant descriptors are styled via the syntax-highlight
   tag, not as widgets. Fixed selectors to match what the plugin
   actually emits, plus added the variable color in concrete hex.

After this, in \`{count, plural, one {1 item} other {# items}}\` you
get: \`count\` and \`#\` highlighted as widgets / colors, \`plural\` and
\`one\` / \`other\` in keyword-green, content in default color.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues surfaced in StringDetails:

1. Clicking Save threw \`DataCloneError: Failed to execute postMessage
   on Window\`. The set-nodes-data payload carries paramsValues, which
   is a Svelte 5 \`\$state\` reactive proxy. The structured-clone
   algorithm browsers use for postMessage doesn't know how to
   serialise that proxy and bails.

   Fix lives in ui/lib/bus.ts: every outbound message is JSON-cloned
   right before parent.postMessage, which strips all Svelte proxies
   into plain objects. Safe because our UI→Main payloads are JSON-only
   (screenshots flow the other way as Uint8Array). All other call
   sites — Settings save, Connect, Push, CreateCopy — benefit from
   this without code changes.

   Also added an explicit \$state.snapshot in StringDetails.save() as
   a belt-and-braces measure / documentation.

2. The translation textarea showed empty on open even when the node
   already had a translation. Root cause: IcuEditor's \`view\` was a
   plain \`let\`, not reactive, so the sync \$effect that mirrors
   external value changes into the CodeMirror doc only ran with
   view===null on first pass and never re-ran after onMount assigned
   the EditorView. Switching to \`\$state<EditorView | null>(null)\`
   makes the effect re-fire once the editor mounts, which then
   correctly dispatches the buffered value into the doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related problems:

1. Save did not change the Figma text. set-nodes-data only writes
   plugin-data — \`TextNode.characters\` stays whatever the layer had
   before. The pull flow already had \`apply-translations\` which loads
   fonts and rewrites characters in addition to plugin-data; we now
   route StringDetails save through that same path.

   - shared/messages.ts: \`apply-translations\` updates can now carry
     pluralParamValue, paramsValues, key, ns, connected so a single
     round-trip persists the full StringDetails payload alongside the
     rendered text.
   - main/nodes/selection.ts applyTranslations: writes every supplied
     field into NodeInfo plugin-data after writeTextNode.
   - main.ts apply-translations handler: re-emits selection after the
     write so the UI sees the fresh NodeInfo (including translation,
     new characters) without an extra fetch.
   - StringDetails.save: renders the ICU translation via
     formatIcuMessage(translation, paramsValues, language) and sends
     apply-translations with both text (rendered) and translation
     (raw ICU source).

2. IcuEditor textarea showed empty when the node already had a
   translation. \`EditorView\` is a class instance — wrapping it in a
   \`\$state\` proxy is unsafe (proxy breaks CodeMirror's internal
   invariants). Switched to \`\$state.raw<EditorView>\`: still tracked
   so the sync effect re-runs once view is constructed, but no proxy
   layer. Also rewrote the sync effect to read both reactive deps
   (value + view) up-front so the early-return path doesn't drop
   value from the dependency set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a TextNode carries both a Tolgee annotation and a manually-added
one from another source, toggling annotations off threw a Figma error
and stopped the whole sync.

Two defenses:

1. \`normalizeAnnotation\` rebuilds each preserved entry as a fresh
   plain object containing only the documented Annotation fields
   (label, labelMarkdown, properties, categoryId). The read-only
   Annotation objects Figma returns from \`node.annotations\` may carry
   internal markers that the setter can't round-trip; round-tripping
   through a clean object avoids that.

2. \`writeAnnotations\` wraps the \`node.annotations = ...\` setter in
   try/catch, logs a warning, and continues iterating instead of
   aborting the whole sync. A single misbehaving node no longer
   blocks the toggle from clearing the rest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix used a normalize step that rebuilt every preserved
Annotation entry as a fresh plain object. That looked safe but caused
silent reverts on nodes with foreign manual annotations: the Figma
setter accepted the assignment without throwing, yet the actual array
on disk stayed unchanged — likely because the clone dropped internal
markers Figma uses to recognise an existing annotation, so the setter
treated everything as a new insert and ignored the operation.

Switched to assigning the read-only Annotation objects back verbatim.
Try/catch around the setter still protects us against genuine throws.
A manual annotation alongside our Tolgee tag now survives the toggle
and the Tolgee tag is actually removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues showed up after a successful push:

1. The overlay rectangles Tolgee draws on uploaded screenshots
   appeared in the wrong place. We were exporting frames at scale 2
   while sending node positions in 1x design-space coordinates — so
   every box landed at half-size and shifted from the visible text.
   The legacy plugin exports at 1x for this reason. Reverted
   EXPORT_SCALE to 1.

2. The Push view re-listed already-pushed plural keys as "changed"
   even though nothing had changed. Tolgee re-emits plurals in a
   canonicalised form that extracts a common suffix out of every
   variant, e.g. our pushed
     \`{n, plural, one {One dog is here.} other {# dogs are here.}}\`
   comes back as
     \`{n, plural, one {One dog is} other {# dogs are}} here.\`
   These render identically for every plural case but the strings
   (and the parsed TolgeeFormat trees) differ — our isChanged check
   correctly saw the mismatch and reported a change.

   Added pluralRendersAreEqual: when both forms claim plural, we
   format the ICU on both sides via intl-messageformat against a
   small set of probe values (0/1/2/5/11/100) and consider them
   equal when every output matches. Only when the rendered outputs
   actually differ do we still flag a change.

   extractParam pulls the plural parameter name from either source
   so the probe-render uses the right placeholder; falls back to
   "count" when neither side declares one. Wrapped in try/catch so
   malformed ICU doesn't crash the diff.

87 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The diff between Figma and Tolgee compared \`textOfNode(node)\` —
which falls back to \`node.characters\` when \`translation\` is empty —
against the raw text Tolgee returns. On the first push of a connected
node we never wrote the pushed string into plugin-data, only marked
the node connected. So the next push diff compared the raw rendered
characters against Tolgee's canonicalised plural form (suffix
extracted out of every variant) and reported every plural key as
"changed" forever.

After a successful push we now persist \`translation: n.translation ||
n.characters\` alongside \`connected: true\`. Subsequent diffs run
local vs. remote with both strings rooted in the same source, and
pluralRendersAreEqual catches the remaining case where Tolgee's
canonicalisation produces an equivalent-but-different ICU shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Tolgee annotation category already gives these entries a distinct
identity in the annotation panel — wrapping the key in \`**...**\` was
visual noise. Plain text reads cleaner and matches how other plugin
annotations typically render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix stored the locally-pushed translation string into
plugin-data. That still produced spurious "changed" diffs whenever
Tolgee rewrote the ICU we sent — e.g. extracting a shared suffix
out of every plural variant — because our cached source no longer
matched what the server now holds.

Now: after a successful push, refetch the same keys from Tolgee
(/v2/projects/translations filtered by the keys we just pushed) and
store the server's canonical translation as the node's translation
property. The next diff then compares like for like; no more
phantom plural diffs.

Best-effort: if the refetch fails, fall back to the locally-pushed
text so connected nodes never end up with an empty translation field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Even with identical translation strings, the diff still reported
"changed" when the local node's \`isPlural\` flag was false while
Tolgee inferred \`keyIsPlural: true\` from the ICU shape. The
isChanged check has a dedicated post-string-compare branch that
fires whenever those flags differ.

Now fetchCanonicalAfterPush returns \`{ translation, isPlural }\` and
we write both into plugin-data after a successful push, so the next
diff has a consistent plural-flag on both sides. Falls back to the
local value when no remote entry is available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Header component had props for languages / namespaces / branches
but nothing fed them. The language Select only showed the currently
saved value (\`de\`) with no other options to switch to.

App.svelte now fetches /v2/projects/languages and
/v2/projects/used-namespaces right after a successful auth bootstrap
and stores the lists on the auth store. Errors are swallowed (the
header falls back to "current value only" when the lists are empty).

Index.svelte maps those lists into Select option shape and forwards
them to Header along with branchingEnabled. Selecting a new language
fires set-language as before; the main thread persists it and emits
config-changed which the rest of the UI reacts to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the header language Select fired set-language directly,
which persisted the new language and emitted config-changed — but
nothing routed the user to a pull view, and the Pull route stayed
stuck on its initially-bootstrapped language until it was remounted.

New flow matches what you asked for:

- Header.handleLanguageChange navigates to Pull with the new language
  in the route. No set-language is sent. The saved language is
  untouched.
- Pull.applyChanges does the persist: when language differs from the
  saved one, it sends set-language right before triggering the
  apply-translations round-trip. So picking the language and clicking
  Apply commits both the language and the translation writes
  together.
- Cancelling Pull (goBack) leaves the saved language intact — the
  header Select snaps back to the persisted value the next time the
  view repaints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull always sent \`filterNamespace: [namespace]\` where \`namespace\` fell
back to the empty string when the document hadn't configured one. On
the Tolgee API, \`filterNamespace: [""]\` filters to the default
namespace only — every key stored under a real namespace got dropped.
So after switching language the diff was always "no changes" simply
because we never fetched the relevant keys.

- fetchAllTranslations now accepts \`namespaces\` as optional. Empty
  array or omitted → a single paginated pass with no
  \`filterNamespace\` query param, so the server returns every key in
  every namespace.
- Pull.svelte forwards \`undefined\` when \`config.namespace\` is empty
  and a singleton array otherwise. CreateCopy still passes its own
  list explicitly (uses the pinned namespace + the default fallback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy plugin classified a connected node with no translation in
the requested language as "missing" — that signal nudges the user to
go translate the key in Tolgee. Our diff was bucketing it as
"unchanged", so switching to a language that has gaps showed "no
changes" and left the user thinking nothing needed pulling.

Two changes in pullDiff:

1. Empty / falsy remoteText (key exists but no string for this
   language yet) → missingKeys, mirroring the legacy behaviour. We
   still don't push an empty string onto the canvas.
2. New legacy-equivalent branch: when the cached translation matches
   the remote but the rendered \`characters\` have drifted (e.g.
   someone retyped the layer manually) AND the key is non-plural
   without params, classify as changed so we can re-apply the source
   of truth.

Test "treats empty remote as unchanged" updated to "classifies as
missing", reflecting the new contract. All 87 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ZuzanaOdstrcilova and others added 24 commits August 19, 2026 12:46
…opping them

eweren's Medium #8. A frame whose export threw was caught, `console.warn`'d
(dev-only, invisible to users) and skipped. `captureScreenshots` returned only
the delivered count, and the push summary printed that as though it were the
whole set — "N screenshot(s) uploaded", with nothing to say M others never
made it. A designer would have to notice the missing screenshots in Tolgee to
find out.

Count the failures alongside the deliveries, carry them on `screenshots-done`,
and say so in the summary: "N screenshot(s) uploaded, M failed to capture."

The counter resets at the start of each push — screenshots are optional, so a
run with the toggle off would otherwise inherit the previous run's failures in
its summary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…blocked

Two of eweren's Medium findings in the copy flows.

#10 — Recreate was re-entrant across the unmount it is designed to survive.
`finishCopyRecreate` deliberately outlives the view that started it, because
recreating deletes the current page and unmounts CopyView. The "busy" flag did
NOT: it lived in component state, so a second click landed on a fresh instance
whose own `stage` read "idle". Since recreating DELETES the existing copy page,
that second run would remove the page the first was midway through writing.

Track the in-flight job beside the other module-scoped state that already
survives the unmount, and check it in `recreateCopy` BEFORE sending
`create-copy` — by the time the continuation registers, the main thread is
already deleting. An older job settling can't clear a newer one's mark.

The fire-and-forget `void (async … )()` also gains a `.catch`: a throw in the
apply chain surfaced only as an unhandled rejection, leaving the view stuck on
"recreating" with no message.

#11 — `hasMissingFont` skipped a layer with nothing to show for it. Such a
layer keeps its ORIGINAL translated text instead of the key label, so a keys
copy came out looking like the plugin had quietly failed on exactly those
layers. `writeTextSafely` now reports whether it wrote, and the names are
collected into `CreateCopyResult` and counted in the completion notice:
"Created keys page — N layer(s) skipped (missing fonts)."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… list

Codex P2 on `pickers.ts:79`. `openapi-fetch` RESOLVES on 4xx/5xx with an
`error` field instead of throwing, and `fetchBranches` destructured only
`data` — so a rejected or failed request returned `[]` on the SUCCESS path.

`hydrateBranches` then set `branchesLoaded: true`, and with a branch
configured `isConfiguredBranchMissing` announced that it had been deleted in
Tolgee, offering an empty picker to replace it. Its own `catch` was there to
prevent exactly this ("a failed fetch says nothing about which branches
exist") — it just never fired, because nothing threw.

Throw on `error`, so the failure reaches the `loaded: false` branch that was
already written for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rs panel

Codex P2 on `scan.ts:199`. With "ignore hidden layers including children"
enabled, the recursive selection scan started every selected root at
`ancestorHidden = false`. The walk only ever looks downward, so nothing ABOVE
the selected root was examined: picking a visible text node or nested frame
from the Layers panel while one of its parents was hidden let that text show
up and be synchronised, while a page-wide scan correctly excluded it via
`computeAncestorHidden` — the same helper, just never applied here.

Seed the traversal with the root's own ancestor state. `computeAncestorHidden`
already walks up to the page and is widened from `TextNode` to `SceneNode`,
which is all it ever needed (it only reads `parent`).

Verified the test fails without the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex P2 on `clipboard.ts:80`. When `execCommand` didn't fire the copy handler
but `navigator.clipboard` existed, the fallback ran `void clip.writeText(text)`
and returned `true` immediately. `writeText` rejects whenever the document
lacks focus or clipboard permission, so the caller flashed its confirmation
while the clipboard still held its old contents — the same "looks like it
worked" failure the rest of this module exists to avoid.

Await it. The function becomes `async`, which does NOT weaken the
"call synchronously from a user gesture" contract it documents: everything up
to the first `await` — the whole `execCommand` path — still runs inside the
gesture's own tick, and the only await is in the fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…literal

Codex P2 on `keyFormat.ts:141`. An empty placeholder dropped the entire
adjacent literal PART, so any fixed text in it went with the separator:
`foo{component}bar{elementName}` with no component produced `fooName`, losing
`bar`. These values are persisted as Tolgee keys and uploaded, so the loss is
silent and durable.

Trim one separator CHARACTER instead. "Separator" is anything that isn't a
letter or a digit — `\w` would keep `_`, which is the separator most templates
actually use.

Worth recording that all three candidate behaviours differ here. The published
plugin (`preformatKey.ts:30`) removes everything between the previous `}` and
the placeholder, giving `barName` — and on `app.{component}.{elementName}` it
eats the user's `app.` prefix entirely. This rewrite already diverged from
that deliberately (its doc comment cites `{component}.{elementName}` →
`element_name`, not `.element_name`), and the change keeps every documented
case identical: all 25 existing tests pass untouched. It differs from the old
behaviour ONLY where a literal carries text beyond its separator, which is the
case being fixed.

Migration impact is limited by `prefillKey`: a key already assigned is never
regenerated by a format change, so this affects newly generated keys only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lide

Codex P2 on `CreateCopy.svelte:102`. Composite `(namespace, key)` map indexes
were built as `${ns}|${key}`, but `|` is legal in both a namespace and a key
name and neither input forbids it — so `(ns "a|b", key "c")` and
`(ns "a", key "b|c")` both spell `a|b|c`. The later entry overwrote the
earlier, and in the copy flow `buildCopyUpdates` then wrote that one
translation onto BOTH cloned nodes.

The report named one site; the pattern was in seven — `pushFlow`'s
`canonicalKey`/`resolutionKey`, `pullDiff`'s remote index and its lookup,
`pushDiff`'s conflict grouping, `copyApply`'s translation lookup, and the two
copy views that BUILD the map `copyApply` reads. Those last two are why this
had to be done in one go rather than at the reported line: the index is
constructed in one file and consumed in another, so changing either alone
silently finds nothing.

All of them now go through `nsKeyIndex`, which joins on NUL — impossible in a
Tolgee key or namespace, and the separator the published plugin uses for its
own composite keys. Tests assert through the helper rather than a literal, so
the separator can change again without a sweep.

Also carries a fixup to 027c8f2: `dispatchCreate`'s return type never declared
`skippedMissingFont`, a type error that shipped because NOTHING in the gates
checks `.svelte` files — `tsc --noEmit` skips them and biome doesn't process
them at all. Found by running the installed-but-unwired `svelte-check`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in the gates looked at `.svelte` files. `tsc --noEmit` skips them,
biome doesn't process them ("No files were processed"), and components have no
unit tests — so for a PR that is largely a Svelte rewrite, the largest part of
the diff was verified by nothing. `svelte-check` was already a dependency; it
was simply never called.

Found the hard way: an `await` written into a non-async arrow — a syntax error
— passed all three gates as green, and a type error in `CreateCopy.svelte`
reached a commit (fixed in 86b4275).

Wires it in as `typecheck:svelte` plus a CI step, and clears the 7 pre-existing
errors that were keeping it red:

  - `Index.svelte` — unused `TolgeeConfig` import.
  - `Index.svelte` — `checkedKeys` declared its own `{ name; ns? }` shape while
    `fetchMissingKeys` takes `ConnectedKey`, whose `ns` is required-but-
    nullable. Same values at runtime (`connectedKeySig` normalises undefined
    and "" to one signature); the two declarations just never agreed. Now uses
    the imported type.
  - `Index.svelte` — `placeholderData` typed its argument `ReadonlySet` where
    the query yields `Set`.
  - `Connect.svelte` / `StringDetails.svelte` — `sel[0]` is `T | undefined`
    under `noUncheckedIndexedAccess` even behind `sel.length === 1`. Added a
    fallback that cannot run, rather than an assertion.
  - `CopyView.svelte` / `CreateCopy.svelte` — `aria-label` on `Select`, which
    didn't declare it. Kept the label and taught the component to forward it:
    a disabled single-option select has no visible label, so that attribute is
    its only accessible name.

None of the seven was hiding a bug — they are type-hygiene, and the `ns` one
(the only candidate) turned out to be two declarations of the same runtime
shape.

Also collapses a duplication this branch introduced: `connectedKeySig` was
already joining `(ns, name)` on NUL for exactly the same purpose as the
`nsKeyIndex` added in 86b4275. It now delegates, so the two can't drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…being overwritten

I had this backwards and want the reasoning recorded rather than repeated.

Chasing "a user migrating from the published plugin can end up with nodes
whose cached translation matches the remote while the canvas is stale, and
nothing ever rescues them", the obvious fix looked like relaxing the
plural/params exclusion in `pullDiff`'s drift branch — `canvasDrifted` renders
with the node's OWN sample, so it seemed safe.

It isn't. Drift is measured against that sample, and a node migrated from the
published plugin has no usable one: that plugin stored the argument NAME in
`pluralParamValue` (its own bug, already noted in interpolate.ts), which
`numericCount` rejects, so the sample falls back to 1. Measured directly: a
migrated layer reading "10 days" renders as "1 day". Relaxing the exclusion
would flag every such layer as drifted and rewrite the design to the wrong
plural form — on every pull, silently. The exclusion is the thing preventing
that, not caution to be tidied away.

So: no behaviour change. The comment now says WHY the exclusion exists (it
previously only said re-applying was "safe" for the other cases, which reads
as an omission rather than a decision), and three tests pin it — a migrated
plural layer and a parametric one must stay untouched, a genuinely drifted
plain layer must still be re-applied. Verified the first fails if the
exclusion is removed.

The underlying gap is real but not fixable here: the sample data is wrong, not
the diff. Repairing it from the canvas (`inferPluralCount` already does
exactly this at connect time) would fix the cause without touching the design
— worth doing deliberately, not as a side effect of a diff tweak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found while fixing the plural seeding in ff1d7bb, not reported by either
reviewer: `namedPlaceholders` matched arguments with
`\{\s*(\w+)\s*(?:,[^{}]*)?\}`, and `[^{}]*` cannot span a nested brace. Any
argument whose body holds braces — every `select`, and a plural wrapped in one
— was therefore never seen and never seeded, so `IntlMessageFormat` threw
MISSING_VALUE on it: the node was dropped in `copyApply` and Pull left stale
text on the canvas. Same failure as the plural fix, one argument type over.

Replaced with `findArgumentNames`, a brace-aware scan sharing the walker that
`findPluralParameters` already used.

That walker needed one thing it didn't have. Inside a plural or select, the
immediate `{…}` runs are variant BODIES, not arguments — in
`{gender, select, other {x}}`, `{x}` is the body of `other`, and collecting it
would invent a parameter named `x`. It now tracks whether it is inside a
variant list, so bodies are descended into but not reported, and
`one {Hello {name}}` still yields `name`. My own new tests caught this; the
first version reported `x` and `hi` as arguments.

The interpolate test that documented this gap by working around it (asserting
on params because the full render couldn't succeed) now asserts the render.

Measured, since this puts a walk where a regex was, on a per-node path: plain
text is FASTER (0.09 vs 0.16 µs — it bails on the first scan with no braces),
a typical parameterised string is identical, a plural is 3x slower at 0.47 µs,
and the worst realistic case is 4.6 µs. Beside the ICU render on the same path
that is noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last of the review's flagged test gaps, and the PR description's own
headline trade-off: the published plugin raised a conflict on ANY plural-form
difference, which drowned the legitimate case (each layer pluralised from its
own sample carries only the form its count selects). Suppressing that was a
product decision — but it means a REAL disagreement inside one category now
resolves first-wins, silently, and nothing pinned that down. Only
non-overlapping categories were covered.

Four tests: the two layers don't conflict; the first layer's body wins for
every shared category and the second's text is dropped (asserted, because it
is accepted behaviour rather than an oversight); non-shared categories are
still unioned, which is what the merge exists for; and the regenerated ICU
still parses with a nested argument intact.

That last one confirms the reviewer's hunch that a test here would have caught
the escaping bug: reverted to the pre-7bc393e `escapeIcuBraces` and it fails,
because `mergePluralForms` regenerates the whole string through exactly that
path and `{name}` came back as literal text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…list loads

Takes the real half of eweren's Medium #9 and declines the other half, with
the reasoning recorded so the disagreement doesn't have to be relitigated.

The real half: `LANG_TAG_RE` accepted any 2-3 lowercase letters, so renaming
"Home - cs" to "Home - wip" was read as a language — silently overriding the
immutable `copyLanguage` marker and breaking Download for that page. Now the
suffix must be a KNOWN project tag whenever that list has loaded.

The shape check remains for the empty-list case, which is not a detail: on a
copy page the language list is usually still in flight when Recreate/Download
runs. The review's suggested fix — constrain the pattern to the project's
actual tags, full stop — would reject a legitimate "cs" there and fall back to
the main language, which is precisely the bug c3a09f6 fixed.

The other half was marker-first precedence, and the name must keep winning:

  - A copy made by the PUBLISHED plugin has no marker to prefer.
    `src/main/endpoints/copyPage.ts:27` stores `{pageCopy, pageInfo, language}`
    and nothing else, and that `language` is the repointable Push/Pull one —
    the very value this resolution exists to distrust. Marker-first would send
    those copies straight to it. The name (`copyPage.ts:17`) is the only stable
    signal such a copy leaves.
  - Name-first is self-healing. Copies whose marker was poisoned to the main
    language before the resolution was fixed recover as soon as the name is
    read; marker-first would keep them broken indefinitely.

`CopyView.svelte`'s comment claimed marker-first while the code did the
opposite — the contradiction the review reasonably picked up on. It now
describes the actual order and why.

Tests cover both halves, including the two cases that decide the precedence: a
poisoned marker losing to a correct name, and a published-plugin copy with no
marker at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d024638 was staged with `git add -A` instead of scoping it to `src/`, so three
untracked things rode along that have nothing to do with this branch:

  - `delivery/tolgee-plugin/killer-decathon.png` (743 kB) — a stray image
  - `.claude/{settings,launch}.json` — local Claude Code config, machine paths
    and all; now gitignored so it can't happen again
  - `pnpm-workspace.yaml` — untracked here before this branch; whether it
    should be committed is a separate decision, not one to make by accident

Untracked, not deleted: the files stay on disk exactly as they were.

This removes them from the branch TIP only. The blobs remain reachable in
d024638 itself, so the 743 kB is still in history — stripping that needs a
rewrite of the range, which is worth doing while the branch is unpushed but is
not something to do unannounced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sweep in 86b4275 missed three sites, and the reason is worth recording:
`nsKeyIndex` was put in `$ui/lib/logic`, which `src/main` cannot import from.
So the main thread kept its own `${ns}|${key}` join and nothing pointed at it.

Moved to `$shared/keyIndex`, where both threads can reach it, and the
stragglers converted:

  - `createCopy.ts` — the per-key counts behind copy staleness. The worst of
    the three: staleness compares COUNTS, so a collision doesn't mislabel a
    key, it cancels a real difference out. Source `(ns "a|b", key "c")` and
    copy `(ns "a", key "b|c")` both spelled `a|b|c`, both sides counted 1, and
    the copy read as up to date while having gained AND lost a string.
  - `Index.svelte` — the `{#each}` key of the unconnected-match list.
  - Stale comments in `createCopy.ts` and `CreateCopy.svelte` still describing
    the old format.

Tests drive the collision through `checkCopyStaleness` and assert both counts;
verified they fail against the `|` join.

Found by auditing for leftover joins rather than by a test — nothing had
covered the main-thread path, which is how it survived the first pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A key changed only by a missing tag, or by the plural flag, rendered as the
usual strike-through diff — with the same string on both sides. There was no
way to tell that from a broken diff.

That is not a hypothetical readability nit. It is what produced eweren's
"push always shows changes" report, and it cost a round of manual testing
yesterday to establish that a screenshot of three identical-looking rows was
CORRECT behaviour (tag sync on, tag not yet on those keys) rather than the bug
it looked like.

`isChanged` becomes `changeReason`, returning "text" | "plural" | "tags" or
null, and `ChangedKey` carries it. Text wins when several apply — the
strike-through shows real content, where a tag note would hide it.

The row keeps the diff for a text change and replaces it otherwise with what
actually happens on push: "Text unchanged — the configured tag will be added."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`delivery/tolgee-plugin/` was still the Jul 24 build while the PR description
states it mirrors `dist/` byte-for-byte — so the claim had been false for the
whole review cleanup. Now md5-identical to `dist/` again, and the zip is
rebuilt from it.

The stray `killer-decathon.png` sitting in that folder is explicitly excluded
from the archive; it was never part of the package and only ended up nearby.

`manifest.json` is unchanged, and `APP_VERSION` stays 2.0.0: the policy in
7affe32 is to bump on new functionality, but 2.0.0 IS this PR and has not
shipped, so there is no released version to move away from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The specs look elements up by their visible label, and this branch renamed
Push → Upload and Pull → Download in the UI (a deliberate call: the users are
designers, so only the labels moved — routes, files and test names stay
push/pull). The specs never followed, so they were searching for buttons that
no longer exist and timing out.

55 locators across 6 spec files, plus one comment quoting the old heading.
Nothing about the plugin was wrong; the specs were out of date.

This does NOT make the suite green. A second layer of drift shows up behind
it — `connect.spec.ts` waits for "1 selected" where the Index now reads
"1 string" — and there are likely more, since each round surfaces another.
Left for a separate pass rather than widened into the review cleanup.

Worth fixing the cause first: `gh pr checks` reports no checks have EVER run
on this branch, which is why months of label changes could drift unnoticed.
The workflow does define an `e2e` job; it has simply never executed here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hare

Second pass over the drift. `branches.spec.ts` is fully green again (9/9 in
5.6s, down from 5 failures and 2.5 minutes of timeouts); the shared label fixes
move the other specs forward without finishing them.

What was actually wrong in branches.spec.ts — none of it a plugin bug:

  - The mocks were `{ times: 1 }`, but the plugin POLLS the branch list. Calls
    two and three fell through to the real API, which 400s on a project with
    branching disabled, and the empty result wiped the list the first call had
    populated. Both intercepts are unbounded now, and documented as such.
  - A test asserted a namespace select in the header. That was removed in
    ba90c02 — the picker lives on the rows and in the bulk action now — so the
    assertion was checking UI that no longer exists. Dropped, with a note.
  - Unbounded intercepts outlive the assertions, so a poll in flight at
    teardown made the handler throw on a closed page. Both handlers now
    tolerate it.

Shared drift fixed across specs: `1 selected` → `1 string` (61), the connect
row action `getByTitle("Connect to existing key")` →
`getByRole("button", { name: "Connect to key" })` (15, it is an aria-label now,
not a title), and the Connect view heading (9).

`connect.spec.ts` is NOT fixed and needs rewriting rather than repair: it
drives a key input, a Connect button, a Cancel button and a plural toggle, and
that view is search-driven now — none of those exist. Same likely for others.
Left alone rather than guessed at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…reen

Second half of the mechanical repair. 62 passing, up from 55 this morning and
from near-zero before any of it. `pull`, `push`, `page-setup` join `branches`,
`copy-view` and `dev-mode` at zero failures.

Two of these were not label drift but a semantic change the fixtures had not
followed, and they are worth naming:

  - `push`: two tests built a "new key" node as `connected: true`. A CONNECTED
    node whose key is absent remotely means the key was DELETED on the
    platform, and `pushDiff` deliberately withholds those rather than silently
    re-creating what someone removed — so the diff had nothing to push and the
    button was right to stay disabled. Fixtures now say `connected: false`,
    which is what "a new local key" actually looks like.
  - `push`: "disabled when no changes" ignored `screenshotOnlyUpload` — with
    the screenshot toggle on and unchanged keys present the button stays
    enabled BY DESIGN, so the test now turns the toggle off to reach the state
    it means to assert.

Also: `getByText("Unsaved changes")` hit a strict-mode violation (the dialog
heading and its body both match) — the dialog was showing all along; the
locator was just ambiguous. And StringDetails' key is a textbox VALUE now, not
free text.

Still failing, all needing a REWRITE rather than a fix, because the views were
redesigned: `namespaces` (14), `settings` (4, tabs restructured from four to
three), `connect` (3), `index` (2, the page-wide count when nothing is
selected no longer exists), `create-copy` (2), `onboarding` (2),
`string-details` (2, plural is locked in that fixture now).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…exists

14 of its 15 tests drove surfaces that are gone: a namespace <select> in the
header (removed in ba90c02 — the picker moved onto the rows and the bulk "Set
namespace" action) and `#connect-ns` / `#connect-key` in a Connect view that is
search-driven now. Repairing locators could not help; the design changed.

Replaced with 7 tests covering where a namespace is actually visible today:

  - Settings Project tab offers a default-namespace FIELD (a free-text input
    with autocomplete, not a dropdown) when namespaces are enabled, and none
    when they are disabled.
  - Connect search results carry `ns:<name>`, spell out `ns:<none>` for the
    default namespace, and drop the badge entirely when the feature is off.
  - An unconnected Index row exposes the per-row namespace input — and does
    not when the feature is off.

The project-meta intercept is unbounded, like the branches one: the plugin
re-reads meta, so `{ times: 1 }` let later calls fall through to the real
(enabled) value and the "disabled" tests never reached the state they name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old file drove a "Test Connection" button, a top-right "Close" button and
four tabs including "Connection" and "Sync". None exist: Settings has THREE
tabs (Project / Strings and Keys / Upload options), a back arrow in the header,
and Cancel + Save in the footer. Credentials are validated by SAVING now, not
by a separate probe, so the two connection tests assert the outcome instead —
landing on Index connected, or staying "Not connected" for a bad key.

Two markers needed care: the "Key format" field only renders once "Prefill key
name" is on, so it is the wrong thing to assert a tab switch with. The tab test
uses the unconditional toggle label, and the save test turns that toggle on —
which is both a real change to persist and what reveals the field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wizard has no "Set up Tolgee" heading and no "1/3" counter — it opens on
a "Connection" step with a numbered stepper mirroring the Settings tabs.

The walk to the end no longer counts Next clicks either: there are four steps
now, and where the wizard starts depends on how much of the config already
validated, so it advances until Save is offered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- index: the page-wide 'N on this page' count and its empty state are gone —
  with no selection the Index asks for one, the same scoping change Download
  got. Asserts that instead.
- create-copy: the mode radios are `sr-only`, so Playwright cannot click
  them; clicks the visible label inside the <label>.
- string-details: Plural is LOCKED for a connected key, so the toggle test now
  opens an unconnected node; the control is a CheckboxField <button>, not
  `#string-details-plural` (which never existed), and the dialog's Save is
  scoped to the dialog so it stops colliding with the footer's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old file drove a free-text key field, a namespace field, a plural toggle
and Connect/Cancel submit buttons. The view searches Tolgee and connects to a
RESULT now — none of those existed, so re-pointing locators could not help.

Seven tests over what the screen actually does: it opens with the search box
PREFILLED from the node's own text (so it lands mid-search, not on the empty
prompt), a no-match search says so, results carry per-row Connect, connecting
navigates back with the node linked, an already-connected key offers
Disconnect instead, and the back arrow returns to Index.

A connected row has no "Connect to key" action, so the helper reaches the view
through the overflow menu's "Connection detail" for that case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 442 files, which is 342 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 3618ff49-2f3f-4983-9f74-142ec5e7bd5f

📥 Commits

Reviewing files that changed from the base of the PR and between 09d2795 and 1fc1d5b.

⛔ Files ignored due to path filters (5)
  • delivery/tolgee-plugin.zip is excluded by !**/*.zip
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src/ui/client/apiSchema.generated.ts is excluded by !**/*.generated.*
  • src/ui/lib/api/schema.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (442)
  • .eslintignore
  • .eslintrc.json
  • .github/workflows/test.yml
  • .gitignore
  • .prettierignore
  • .prettierrc.json
  • biome.json
  • cypress.config.mjs
  • cypress/common/tools.ts
  • cypress/e2e/copyView.cy.ts
  • cypress/e2e/index.cy.ts
  • cypress/e2e/keyFormatEditor.cy.ts
  • cypress/e2e/language.cy.ts
  • cypress/e2e/pageSetup.cy.ts
  • cypress/e2e/pull.cy.ts
  • cypress/e2e/push.cy.ts
  • cypress/e2e/settings.cy.ts
  • cypress/e2e/stringDetails.cy.ts
  • cypress/fixtures/example.json
  • cypress/support/commands.ts
  • cypress/support/custom.d.ts
  • cypress/support/e2e.ts
  • cypress/tsconfig.json
  • delivery/INSTALL.md
  • delivery/tolgee-plugin/INSTALL.md
  • delivery/tolgee-plugin/main.js
  • delivery/tolgee-plugin/manifest.json
  • delivery/tolgee-plugin/ui.html
  • docs/superpowers/plans/2026-07-10-key-format-cursor.md
  • docs/superpowers/specs/2026-07-10-key-format-cursor-design.md
  • e2e/docker-compose.yml
  • e2e/global-setup.ts
  • e2e/host/fixtures.ts
  • e2e/host/index.html
  • e2e/host/main.ts
  • e2e/import-data/examples/cs.json
  • e2e/import-data/examples/de.json
  • e2e/import-data/examples/en.json
  • e2e/import-data/examples/fr.json
  • e2e/import-data/examples/namespaced/cs.json
  • e2e/import-data/examples/namespaced/de.json
  • e2e/import-data/examples/namespaced/en.json
  • e2e/import-data/examples/namespaced/fr.json
  • e2e/import-data/examples/translation/cs.json
  • e2e/import-data/examples/translation/de.json
  • e2e/import-data/examples/translation/en.json
  • e2e/import-data/examples/translation/fr.json
  • e2e/tests/branches.spec.ts
  • e2e/tests/connect.spec.ts
  • e2e/tests/copy-view.spec.ts
  • e2e/tests/create-copy.spec.ts
  • e2e/tests/dev-mode.spec.ts
  • e2e/tests/index.spec.ts
  • e2e/tests/namespaces.spec.ts
  • e2e/tests/onboarding.spec.ts
  • e2e/tests/page-setup.spec.ts
  • e2e/tests/pull.spec.ts
  • e2e/tests/push.spec.ts
  • e2e/tests/settings.spec.ts
  • e2e/tests/string-details.spec.ts
  • e2e/vite.config.host.ts
  • e2e/wait-for-tolgee.mjs
  • eslint.config.js
  • gallery/App.svelte
  • gallery/Copy.svelte
  • gallery/Onboarding.svelte
  • gallery/Screens.svelte
  • gallery/app.css
  • gallery/index.html
  • gallery/main.ts
  • gallery/mock.ts
  • gallery/theme.css
  • package.json
  • playwright.config.ts
  • scripts/build-main.mjs
  • scripts/build-manifest.mjs
  • scripts/runAfterApp.ts
  • src/createFormatIcu.ts
  • src/custom.d.ts
  • src/main/__tests__/bus.test.ts
  • src/main/__tests__/main.test.ts
  • src/main/bus.ts
  • src/main/endpoints/clearPrefilledKeys.ts
  • src/main/endpoints/copyPage.ts
  • src/main/endpoints/editorType.ts
  • src/main/endpoints/formatText.ts
  • src/main/endpoints/getConnectedNodes.ts
  • src/main/endpoints/getScreenshots.ts
  • src/main/endpoints/getSelectedNodes.ts
  • src/main/endpoints/highlightNode.ts
  • src/main/endpoints/notify.ts
  • src/main/endpoints/preformatKey.ts
  • src/main/endpoints/setNodesData.ts
  • src/main/endpoints/updateNodes.ts
  • src/main/handlers/__tests__/createCopy.test.ts
  • src/main/handlers/auth.ts
  • src/main/handlers/createCopy.ts
  • src/main/handlers/screenshots.ts
  • src/main/handlers/settings.ts
  • src/main/main.ts
  • src/main/nodes/__tests__/clearPrefilled.test.ts
  • src/main/nodes/__tests__/filter.test.ts
  • src/main/nodes/__tests__/getNodeInfo.test.ts
  • src/main/nodes/__tests__/highlight.test.ts
  • src/main/nodes/__tests__/nodeParents.test.ts
  • src/main/nodes/__tests__/scan.test.ts
  • src/main/nodes/__tests__/selection.test.ts
  • src/main/nodes/clearPrefilled.ts
  • src/main/nodes/filter.ts
  • src/main/nodes/getNodeInfo.ts
  • src/main/nodes/highlight.ts
  • src/main/nodes/nodeParents.ts
  • src/main/nodes/scan.ts
  • src/main/nodes/selection.ts
  • src/main/screenshots/__tests__/capture.test.ts
  • src/main/screenshots/__tests__/groupByFrame.test.ts
  • src/main/screenshots/capture.ts
  • src/main/screenshots/groupByFrame.ts
  • src/main/screenshots/position.ts
  • src/main/settings/__tests__/index.test.ts
  • src/main/settings/__tests__/merge.test.ts
  • src/main/settings/index.ts
  • src/main/settings/merge.ts
  • src/main/settings/storage.ts
  • src/main/text/__tests__/applyRichText.test.ts
  • src/main/text/applyRichText.ts
  • src/main/utils/createEndpoint.ts
  • src/main/utils/delayed.ts
  • src/main/utils/nodeParents.ts
  • src/main/utils/nodeTools.ts
  • src/main/utils/pages.ts
  • src/main/utils/settingsTools.ts
  • src/main/utils/textFormattingTools.ts
  • src/shared/__tests__/icu.test.ts
  • src/shared/__tests__/interpolate.test.ts
  • src/shared/__tests__/keyFormat.test.ts
  • src/shared/__tests__/messagePolicy.test.ts
  • src/shared/__tests__/tolgeeFormat.test.ts
  • src/shared/constants.ts
  • src/shared/iconSizes.ts
  • src/shared/icu.ts
  • src/shared/interpolate.ts
  • src/shared/keyFormat.ts
  • src/shared/keyIndex.ts
  • src/shared/messagePolicy.ts
  • src/shared/messages.ts
  • src/shared/richText.ts
  • src/shared/tolgeeFormat.ts
  • src/shared/types.ts
  • src/shared/version.ts
  • src/tools/compareNs.ts
  • src/tools/createProvider.tsx
  • src/tools/getConflictingNodes.ts
  • src/tools/getConnectedNodes.ts
  • src/tools/getPullChanges.ts
  • src/tools/getPushChanges.ts
  • src/types.ts
  • src/ui/App.svelte
  • src/ui/Plugin.tsx
  • src/ui/client/apiSchema.custom.ts
  • src/ui/client/client.ts
  • src/ui/client/decodeApiKey.ts
  • src/ui/client/errorCodes.ts
  • src/ui/client/types.ts
  • src/ui/client/useQueryApi.ts
  • src/ui/components/ActionsBottom/ActionsBottom.css
  • src/ui/components/ActionsBottom/ActionsBottom.css.d.ts
  • src/ui/components/ActionsBottom/ActionsBottom.tsx
  • src/ui/components/AutocompleteSelect/AutocompleteSelect.css
  • src/ui/components/AutocompleteSelect/AutocompleteSelect.css.d.ts
  • src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx
  • src/ui/components/Badge/Badge.css
  • src/ui/components/Badge/Badge.css.d.ts
  • src/ui/components/Badge/Badge.tsx
  • src/ui/components/BranchSelect/BranchSelect.css
  • src/ui/components/BranchSelect/BranchSelect.css.d.ts
  • src/ui/components/BranchSelect/BranchSelect.tsx
  • src/ui/components/Dialog/Dialog.css
  • src/ui/components/Dialog/Dialog.css.d.ts
  • src/ui/components/Dialog/Dialog.tsx
  • src/ui/components/Editor/Editor.css
  • src/ui/components/Editor/Editor.css.d.ts
  • src/ui/components/Editor/Editor.tsx
  • src/ui/components/Editor/PluralEditor.tsx
  • src/ui/components/Editor/TranslationPlurals.tsx
  • src/ui/components/FullPageLoading/FullPageLoading.css
  • src/ui/components/FullPageLoading/FullPageLoading.css.d.ts
  • src/ui/components/FullPageLoading/FullPageLoading.tsx
  • src/ui/components/InfoTooltip/InfoTooltip.css
  • src/ui/components/InfoTooltip/InfoTooltip.css.d.ts
  • src/ui/components/InfoTooltip/InfoTooltip.tsx
  • src/ui/components/KeyOptionsButton/KeyOptionsButton.css
  • src/ui/components/KeyOptionsButton/KeyOptionsButton.css.d.ts
  • src/ui/components/KeyOptionsButton/KeyOptionsButton.tsx
  • src/ui/components/LocateNodeButton/LocateNodeButton.css
  • src/ui/components/LocateNodeButton/LocateNodeButton.css.d.ts
  • src/ui/components/LocateNodeButton/LocateNodeButton.tsx
  • src/ui/components/NamespaceSelect/NamespaceSelect.css
  • src/ui/components/NamespaceSelect/NamespaceSelect.css.d.ts
  • src/ui/components/NamespaceSelect/NamespaceSelect.tsx
  • src/ui/components/NodeList/NodeList.css
  • src/ui/components/NodeList/NodeList.css.d.ts
  • src/ui/components/NodeList/NodeList.tsx
  • src/ui/components/NodeList/NodeRow.css
  • src/ui/components/NodeList/NodeRow.css.d.ts
  • src/ui/components/NodeList/NodeRow.tsx
  • src/ui/components/Popover/Popover.css
  • src/ui/components/Popover/Popover.css.d.ts
  • src/ui/components/Popover/Popover.tsx
  • src/ui/components/ResizeHandle/ResizeHandle.css
  • src/ui/components/ResizeHandle/ResizeHandle.css.d.ts
  • src/ui/components/ResizeHandle/ResizeHandle.tsx
  • src/ui/components/Shared/HtmlText.tsx
  • src/ui/components/TopBar/TopBar.css
  • src/ui/components/TopBar/TopBar.css.d.ts
  • src/ui/components/TopBar/TopBar.tsx
  • src/ui/hooks/useAllTags.ts
  • src/ui/hooks/useAllTranslations.ts
  • src/ui/hooks/useConnectedMutation.ts
  • src/ui/hooks/useConnectedNodes.ts
  • src/ui/hooks/useCopyPage.ts
  • src/ui/hooks/useEditorMode.ts
  • src/ui/hooks/useFigmaNotify.ts
  • src/ui/hooks/useFormatText.ts
  • src/ui/hooks/useHasBranchingEnabled.ts
  • src/ui/hooks/useHasNamespacesEnabled.ts
  • src/ui/hooks/useHighlightNodeMutation.ts
  • src/ui/hooks/useInterpolatedTranslation.ts
  • src/ui/hooks/usePrefilledKey.ts
  • src/ui/hooks/useSelectedNodes.ts
  • src/ui/hooks/useSetNodesDataMutation.ts
  • src/ui/hooks/useTranslation.ts
  • src/ui/hooks/useUpdateNodesMutation.ts
  • src/ui/hooks/useWindowSize.ts
  • src/ui/icons/SvgIcons.tsx
  • src/ui/index.html
  • src/ui/lib/__tests__/busRequest.test.ts
  • src/ui/lib/__tests__/clipboard.test.ts
  • src/ui/lib/api/__tests__/auth.test.ts
  • src/ui/lib/api/__tests__/branches.test.ts
  • src/ui/lib/api/__tests__/keyExistence.test.ts
  • src/ui/lib/api/__tests__/keys.test.ts
  • src/ui/lib/api/__tests__/keysByName.test.ts
  • src/ui/lib/api/__tests__/pageNodes.test.ts
  • src/ui/lib/api/__tests__/pickers.test.ts
  • src/ui/lib/api/__tests__/projectMeta.test.ts
  • src/ui/lib/api/__tests__/pull.test.ts
  • src/ui/lib/api/__tests__/screenshots.test.ts
  • src/ui/lib/api/__tests__/tags.test.ts
  • src/ui/lib/api/auth.ts
  • src/ui/lib/api/branches.ts
  • src/ui/lib/api/client.ts
  • src/ui/lib/api/keyExistence.ts
  • src/ui/lib/api/keys.ts
  • src/ui/lib/api/keysByName.ts
  • src/ui/lib/api/pageNodes.ts
  • src/ui/lib/api/parentNames.ts
  • src/ui/lib/api/pickers.ts
  • src/ui/lib/api/projectMeta.ts
  • src/ui/lib/api/pull.ts
  • src/ui/lib/api/push.ts
  • src/ui/lib/api/screenshots.ts
  • src/ui/lib/api/tags.ts
  • src/ui/lib/bus.ts
  • src/ui/lib/busRequest.ts
  • src/ui/lib/clipboard.ts
  • src/ui/lib/components/domain/ErrorBanner.svelte
  • src/ui/lib/components/domain/Header.svelte
  • src/ui/lib/components/domain/IcuEditor.svelte
  • src/ui/lib/components/domain/IcuPreview.svelte
  • src/ui/lib/components/domain/IcuTextarea.svelte
  • src/ui/lib/components/domain/NodeList.svelte
  • src/ui/lib/components/domain/NodeListItem.svelte
  • src/ui/lib/components/domain/ParamsEditor.svelte
  • src/ui/lib/components/domain/PluralEditor.svelte
  • src/ui/lib/components/domain/PluralVariantInput.svelte
  • src/ui/lib/components/domain/ProjectMetaSync.svelte
  • src/ui/lib/components/domain/PullSummary.svelte
  • src/ui/lib/components/domain/PushConflictItem.svelte
  • src/ui/lib/components/domain/ResizeHandle.svelte
  • src/ui/lib/components/domain/SettingsSectionConnection.svelte
  • src/ui/lib/components/domain/SettingsSectionKeys.svelte
  • src/ui/lib/components/domain/SettingsSectionProject.svelte
  • src/ui/lib/components/domain/SettingsSectionPush.svelte
  • src/ui/lib/components/domain/SettingsSectionSync.svelte
  • src/ui/lib/components/domain/SyncButton.svelte
  • src/ui/lib/components/domain/ViewFooter.svelte
  • src/ui/lib/components/domain/ViewHeader.svelte
  • src/ui/lib/components/icons/Tolgee.svelte
  • src/ui/lib/components/ui/badge.svelte
  • src/ui/lib/components/ui/button.svelte
  • src/ui/lib/components/ui/buttonContext.ts
  • src/ui/lib/components/ui/card.svelte
  • src/ui/lib/components/ui/checkbox.svelte
  • src/ui/lib/components/ui/checkboxField.svelte
  • src/ui/lib/components/ui/dialog/Content.svelte
  • src/ui/lib/components/ui/dialog/Description.svelte
  • src/ui/lib/components/ui/dialog/Overlay.svelte
  • src/ui/lib/components/ui/dialog/Title.svelte
  • src/ui/lib/components/ui/dialog/index.ts
  • src/ui/lib/components/ui/dropdown-menu/Content.svelte
  • src/ui/lib/components/ui/dropdown-menu/Item.svelte
  • src/ui/lib/components/ui/dropdown-menu/Separator.svelte
  • src/ui/lib/components/ui/dropdown-menu/index.ts
  • src/ui/lib/components/ui/emptyState.svelte
  • src/ui/lib/components/ui/filterChip.svelte
  • src/ui/lib/components/ui/iconButton.svelte
  • src/ui/lib/components/ui/index.ts
  • src/ui/lib/components/ui/input.svelte
  • src/ui/lib/components/ui/keyFormatInput.svelte
  • src/ui/lib/components/ui/label.svelte
  • src/ui/lib/components/ui/message.svelte
  • src/ui/lib/components/ui/namespaceInput.svelte
  • src/ui/lib/components/ui/progressBar.svelte
  • src/ui/lib/components/ui/searchInput.svelte
  • src/ui/lib/components/ui/select.svelte
  • src/ui/lib/components/ui/stat.svelte
  • src/ui/lib/components/ui/statusMarker.svelte
  • src/ui/lib/components/ui/switch.svelte
  • src/ui/lib/components/ui/tabs/Content.svelte
  • src/ui/lib/components/ui/tabs/List.svelte
  • src/ui/lib/components/ui/tabs/Trigger.svelte
  • src/ui/lib/components/ui/tabs/index.ts
  • src/ui/lib/components/ui/tagInput.svelte
  • src/ui/lib/components/ui/tooltip/Content.svelte
  • src/ui/lib/components/ui/tooltip/Root.svelte
  • src/ui/lib/components/ui/tooltip/__tests__/dismiss.test.ts
  • src/ui/lib/components/ui/tooltip/dismiss.ts
  • src/ui/lib/components/ui/tooltip/index.ts
  • src/ui/lib/components/ui/tooltipIconButton.svelte
  • src/ui/lib/components/ui/truncatedText.svelte
  • src/ui/lib/logic/__tests__/asyncPool.test.ts
  • src/ui/lib/logic/__tests__/authBootstrap.test.ts
  • src/ui/lib/logic/__tests__/copyApply.test.ts
  • src/ui/lib/logic/__tests__/copyLanguage.test.ts
  • src/ui/lib/logic/__tests__/copyRecreate.test.ts
  • src/ui/lib/logic/__tests__/deeplink.test.ts
  • src/ui/lib/logic/__tests__/icuParams.test.ts
  • src/ui/lib/logic/__tests__/manualChange.test.ts
  • src/ui/lib/logic/__tests__/namespaces.test.ts
  • src/ui/lib/logic/__tests__/prefillKey.test.ts
  • src/ui/lib/logic/__tests__/progress.test.ts
  • src/ui/lib/logic/__tests__/pullDiff.test.ts
  • src/ui/lib/logic/__tests__/pushDiff.test.ts
  • src/ui/lib/logic/__tests__/pushFlow.test.ts
  • src/ui/lib/logic/__tests__/queryResult.test.ts
  • src/ui/lib/logic/__tests__/sanitizeIcuPreview.test.ts
  • src/ui/lib/logic/__tests__/saveQueue.test.ts
  • src/ui/lib/logic/__tests__/virtualWindow.test.ts
  • src/ui/lib/logic/asyncPool.ts
  • src/ui/lib/logic/authBootstrap.ts
  • src/ui/lib/logic/copyApply.ts
  • src/ui/lib/logic/copyLanguage.ts
  • src/ui/lib/logic/deeplink.ts
  • src/ui/lib/logic/icuParams.ts
  • src/ui/lib/logic/manualChange.ts
  • src/ui/lib/logic/namespaces.ts
  • src/ui/lib/logic/prefillKey.ts
  • src/ui/lib/logic/progress.ts
  • src/ui/lib/logic/pullDiff.ts
  • src/ui/lib/logic/pushDiff.ts
  • src/ui/lib/logic/pushFlow.ts
  • src/ui/lib/logic/queryResult.ts
  • src/ui/lib/logic/sanitizeIcuPreview.ts
  • src/ui/lib/logic/saveQueue.ts
  • src/ui/lib/logic/virtualWindow.ts
  • src/ui/lib/routes/Connect.svelte
  • src/ui/lib/routes/CopyView.svelte
  • src/ui/lib/routes/CreateCopy.svelte
  • src/ui/lib/routes/Index.svelte
  • src/ui/lib/routes/Onboarding.svelte
  • src/ui/lib/routes/PageSetup.svelte
  • src/ui/lib/routes/Pull.svelte
  • src/ui/lib/routes/Push.svelte
  • src/ui/lib/routes/Settings.svelte
  • src/ui/lib/routes/StringDetails.svelte
  • src/ui/lib/stores/__tests__/app.svelte.test.ts
  • src/ui/lib/stores/app.svelte.ts
  • src/ui/lib/stores/auth.svelte.ts
  • src/ui/lib/stores/query.svelte.ts
  • src/ui/lib/utils.ts
  • src/ui/main.ts
  • src/ui/state/GlobalState.ts
  • src/ui/state/sizes.ts
  • src/ui/styles.css
  • src/ui/styles.css.d.ts
  • src/ui/ui.tsx
  • src/ui/views/Connect/Connect.css
  • src/ui/views/Connect/Connect.css.d.ts
  • src/ui/views/Connect/Connect.tsx
  • src/ui/views/Connect/SearchRow.css
  • src/ui/views/Connect/SearchRow.css.d.ts
  • src/ui/views/Connect/SearchRow.tsx
  • src/ui/views/CopyView/CopyView.tsx
  • src/ui/views/CreateCopy/CreateCopy.tsx
  • src/ui/views/Index/Index.css
  • src/ui/views/Index/Index.css.d.ts
  • src/ui/views/Index/Index.tsx
  • src/ui/views/Index/KeyInput.tsx
  • src/ui/views/Index/ListItem.tsx
  • src/ui/views/PageSetup/PageSetup.tsx
  • src/ui/views/Pull/Pull.css
  • src/ui/views/Pull/Pull.css.d.ts
  • src/ui/views/Pull/Pull.tsx
  • src/ui/views/Push/Changes.css
  • src/ui/views/Push/Changes.css.d.ts
  • src/ui/views/Push/Changes.tsx
  • src/ui/views/Push/Push.tsx
  • src/ui/views/Router.tsx
  • src/ui/views/Settings/Expandable.tsx
  • src/ui/views/Settings/ProjectSection.tsx
  • src/ui/views/Settings/ProjectSettings.css
  • src/ui/views/Settings/ProjectSettings.css.d.ts
  • src/ui/views/Settings/ProjectSettings.tsx
  • src/ui/views/Settings/PushSection.tsx
  • src/ui/views/Settings/Settings.css
  • src/ui/views/Settings/Settings.css.d.ts
  • src/ui/views/Settings/Settings.tsx
  • src/ui/views/Settings/StringsEditor.css
  • src/ui/views/Settings/StringsEditor.css.d.ts
  • src/ui/views/Settings/StringsEditor.tsx
  • src/ui/views/Settings/StringsSection.tsx
  • src/ui/views/StringDetails/StringDetails.css
  • src/ui/views/StringDetails/StringDetails.css.d.ts
  • src/ui/views/StringDetails/StringDetails.tsx
  • src/ui/views/routes.ts
  • src/ui/vite-env.d.ts
  • src/utilities.ts
  • src/web/exampleScreenshot.ts
  • src/web/iframeContent.ts
  • src/web/main.ts
  • src/web/ui.tsx
  • src/web/urlConfig.ts
  • tsconfig.json
  • vite.config.gallery.ts
  • vite.config.ui.ts
  • vitest.config.ts
  • web/figma.css
  • web/index.html
  • web/package.json
  • web/src/main.ts
  • web/src/ui.tsx

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands.

v2 replaces the Preact codebase entirely, so every conflict was the same
shape: a file this branch deleted and main went on to edit. All of them
are resolved by keeping the deletion.

The fixes main made in the meantime are not lost — each already has a
native implementation here:

- #103 (derive {component} from instances) → nodes/nodeParents.ts, which
  resolves the main component of an instance and adds {instance} as a
  variable of its own
- #105 (key format cursor) → ui/lib/components/ui/keyFormatInput.svelte
- #100/#98 (scan and font caching) → stores/app.svelte.ts, api/pageNodes.ts

cypress/ and docs/superpowers/ are left out for the same reason: the
rewrite dropped Cypress for Playwright, so a Cypress spec and the design
notes for a Preact fix would document code that no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dkrizan
dkrizan requested a review from bdshadow September 9, 2026 09:52
@eweren

eweren commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Closed my draft PR. Happy to see the figma plugins new version already being live 🙌

@bdshadow
bdshadow merged commit 305b7f3 into main Sep 17, 2026
4 checks passed
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.

4 participants