Skip to content

Release 4.2.9#960

Merged
thiagoralves merged 83 commits into
mainfrom
development
Jul 24, 2026
Merged

Release 4.2.9#960
thiagoralves merged 83 commits into
mainfrom
development

Conversation

@thiagoralves

Copy link
Copy Markdown
Contributor

Promote developmentmain for the 4.2.9 release.

Highlights since the last release:

  • Runtime User Management screen with RBAC (admin/user), gated on runtime ≥ v4.1.9.
  • Version bumped to 4.2.9 (APP_VERSION + package.json, in sync — About dialog and binary agree).
  • Plus the accumulated development fixes.

After merge, tag v4.2.9 on main to trigger the Build and Release workflow.

🤖 Generated with Claude Code

dcoutinho1328 and others added 30 commits June 17, 2026 00:23
Port the byte-identical shared transpiler changes from openplc-web's
ws1-corpus-divergences work: graph-based ComputeConnectionTypes
pin-type inference (new walker/connection-types.ts), blockOverloads /
isOfType helpers, CR-aware pySplitLines, and the ld/fbd typeContext
plumbing. Supersedes the TO_<TYPE> name-suffix conversion stopgap in
pou-graphical with the connection-driven mechanism (190/190 oracle
parity). Keeps backend/shared byte-identical with web.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror web's comparison feature on the editor side. transpileToSt (the
platform port) and compileForDebugger now run BOTH the legacy xml2st
subprocess and the new in-process TS transpiler on every compile, log a
comparison verdict, and use the old engine's output as authoritative
unless OPENPLC_USE_NEW_TRANSPILER flips it. The non-authoritative
engine's failure is non-fatal. Adds an editor-local compare helper
mirroring web's compare-transpiler-output.

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

Byte-identical mirror of openplc-web (PR #528) — keeps the shared
backend/shared/ surface in sync (ci-sync). See the web commit for rationale:
the transpiler now resolves block signatures from each placed block's
node.data.variant instead of the bundled std_block_catalog.json (deleted),
and adds __XWORD to the type lattice and base types.

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

The in-process JSON->ST transpiler is byte-identical to xml2st across the
parity corpus, so drop the dual-run + comparison and the OPENPLC_USE_NEW_TRANSPILER
flag, and stop bundling/downloading the xml2st binary. Tasks now emit in
priority order to match the old XML generator's output (D1). XmlGenerator is
kept for the "Export Project as XML" feature only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The discriminated `pou.body` union already narrows `value` on `language`,
so `as RFBody` / `as RFFbdBody` trip `no-unnecessary-type-assertion` and
fail the lint check (pre-existing on this branch). Remove the casts and
the now-unused type imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap an over-length return statement so the Format Check passes (it only
started running now that the lint gate is green again).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing (#830) (#891)

* fix(transpiler): emit parallel set/reset coils under one shared IF

Parallel coils branch off the same energization path, so the rung
condition must be evaluated once and applied to every branch. A separate
IF per coil re-evaluates the condition between assignments, which is wrong
when the condition reads a coil an earlier branch just set (e.g.
NOT(coils1) ... coils1 := TRUE flips coils2's condition). xml2st has the
same bug; fixed here on the in-process engine.

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

* fix(transpiler): group non-adjacent parallel coils; dedup identical paths

Extends the parallel-coil grouping to the issue #836 topologies: SET/RESET
coils sharing one source now collapse into a single IF even when a
merge-fed coil sorts between them, so a contact feeding multiple coils is
evaluated once instead of re-read between coil assignments. Also dedups
byte-identical parallel paths (X OR X -> X) so the merged condition reads
cleanly. Sequential coils (distinct sources) keep separate IFs by design.

Deliberate divergence from xml2st (same class of fix as D1). Zero output
change across the 193-project LD corpus; covered by a 3-topology regression
test matching the maintainer's expected output.

Closes #836

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

* fix(transpiler): emit block output writes right after the block call

Chained block calls in a rung emitted all calls first, then all output
assignments — so a downstream block read a variable before the upstream
block's write was emitted (Output stayed 0). Couple each output-write sink
(coil / output / inOut variable fed solely by a block) to its block: emit
it immediately after the actual call, eager or lazy, via emitBlockConsumers
(reusing the coil-grouping sweep). Robust to blocks pulled through a later
block's EN expression, which the sink-slot approach missed.

Deliberate divergence from xml2st (same class as #836). 10/193 LD corpus
fixtures corrected (incl. real 4- and 5-block chains), 183 unchanged; #836
grouping + 301 integration tests still pass.

Closes #830

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address CodeRabbit review on #885:
- compileForDebugger ran mkdir/writeFile for program.st outside any try, so a
  disk/permission failure rejected without posting the stop message or closing
  _mainProcessPort (hung debugger). Wrap them in try/catch matching the sibling
  steps.
- Drop async from transpileToSt/computeMd5 (no await; require-await warnings),
  returning Promise.resolve — matches the sibling library-build port.

Editor-only (backend/editor); no shared-surface change.

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

Byte-identical mirror of the shared SoftMotion surface from openplc-web
(PR to be released together):
- cia402.ts: recognize CiA 402 drives + resolve objects to IEC addresses
- generate-softmotion.ts: compile-time AXIS_REF_SM3 globals + PDO scalars
  + per-scan SM_Drive_GenericDS402 bridge + VAR_EXTERNAL injection
- enrichDeviceData tags recognized drives; open-plc/esi-types carry the
  Cia402AxisConfig; preprocessPous runs the codegen in every compile path
- shared tests (cia402, generate-softmotion, preprocess-pous) pass under
  the editor's jest
- binary-versions: strucpp v0.5.13 -> v0.5.14 (ships the SM3 library +
  FB inout copy-back + composite shared globals)

The editor loads plcopen-softmotion.stlib via its bundled-libs directory
scan (no bundled-stlibs.ts change needed). Requires strucpp release v0.5.14
(STruCpp PR #194).

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

The Explorer and Source Control contexts each mounted their own
ResizablePanel (different ids), so every context switch made
react-resizable-panels rebuild the layout from defaultSize and
re-normalize it (the group's defaultSizes sum to less than 100),
inflating the captured getSize() value on every switch until the
32% fixed point. The left slot is now a single always-mounted
ResizablePanel owned by the workspace screen; only its children
swap, so the width is preserved natively and the size-capture
plumbing is gone.

The activity-bar divider under the context buttons was rendered
unconditionally, leaving a stray separator on desktop where the
buttons are hidden (no version control capability). It now renders
only when at least one context button is present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fpthmAmQoNX8WMYyb68Qi
…-issues

fix: keep sidebar size on context switch and hide stray activity-bar divider
Unmodified LD/FBD POUs showed as Modified in Source Control,
drag-away-and-restore produced permanent phantom diffs, and every
project open silently corrupted linked variable pins.

- Persist the raw ladder/FBD flow (Zod-validated, not Zod-stripped) so
  unedited POUs round-trip byte-identical against HEAD
- Make focus/blur drag-lock updates transient so clicking a node never
  dirties the flow
- Canonicalize rung serialization: strip hasDivergence render state,
  normalize draggable, derive reactFlowViewport from content bounds
- Reuse variable-pin node identities across layout rebuilds so
  drag-away-and-restore keeps ids, numericIds, and edge order stable
- Validate variable pins against their block pin type — fixes a
  pre-existing bug that flagged every linked pin wrongVariable and
  rewrote its payload as broken-* on each project open and drag-stop
  (stale flags now self-heal)
- Derive changed-rung indexes from a semantic node/edge diff instead of
  byte equality (openplc-web only)
- Fix stale diff-view HEAD cache: prune entries per-path on save,
  refetch per-file instead of per-map, and resolve commit-with-diff-open
  to the new HEAD

Note: projects saved with the previous serialization show a one-time
Modified diff per graphical POU on first save; committing it once
migrates the file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYE8gdREZQNYQeLfFuuZ9T
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYE8gdREZQNYQeLfFuuZ9T
- Merge instead of replace headContent on diff fetch failure so a
  transient error does not evict other files' cached HEAD entries
- Fetch node/rung fresh inside block.tsx focus/blur handlers (matches
  contact/coil) instead of closing over render-scope values
- Rename loop-local previousNodes to previousLinkedNodes in
  diagram/index.ts to stop shadowing the identity-source parameter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYE8gdREZQNYQeLfFuuZ9T
…ource-control-drift

fix: eliminate phantom source-control diffs in graphical editors
…riables

A variable with an interface class (input/output/inOut/external/temp)
and a physical location ("AT") is invalid IEC 61131-3. The editor let
users create one anyway — the location cell's selected branch skipped
the read-only guard — saved it, and on reopen the ST parser rejected
the declaration, silently discarding every variable of the POU
(GitHub issue #904).

- variables-table: honor the read-only rule in the location cell's
  selected branch so a location can't be attached to an
  interface-class variable
- store validation: reject a location update on an interface-class
  variable, clear the location when the class changes to one, and
  strip it on create, so non-table callers keep the same invariant
- project load: push a warning (surfaced in the console panel) when a
  POU fails to parse instead of silently loading it with no variables
- parser: include the offending line and a repair hint in the
  located-declaration error so the code-view repair path is actionable

Fixes #904 (DOPE-445)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsPXB6bqvFU4RLBQ6snwDp
When a project with unsaved changes was open and the user triggered
File -> Open, the save-changes modal's 'open-project' case discarded
the picker result, so the selected project never loaded. Dispatch
handleOpenProjectResponse with the result, mirroring the accelerator
handler and start-screen flows (DOPE-447).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQFVaMMwCZ77YYJKHSaWRM
The code-mode pass in handleOpenProjectResponse ran after the auto-open
block had already added and activated a default table-mode model for the
same POU (it prefers "main" — exactly the POU most likely to carry the
unparseable variables). addModel no-ops on duplicates and setEditor
early-returns when the name matches the active editor, so the preserved
raw text never reached the model and the code view showed an empty
VAR/END_VAR skeleton instead of the declarations to repair.

Route the raw text through editorActions.updateModelVariablesForName,
which updates whichever object holds the POU — the active editor or the
stored model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsPXB6bqvFU4RLBQ6snwDp
fix: File -> Open did nothing when open project had unsaved changes (DOPE-447)
The fallback POU for unparseable variables carries the raw declarations
in a top-level variablesText field, which the save serializer treats as
authoritative over the structured variables. The commit-to-table guard
looked for the field inside pou.interface (the old backend location),
so it never cleared it — after repairing the declarations in code view
and switching to table view, any table-mode save wrote the stale broken
text back to disk, resurrecting the corruption on reopen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FsPXB6bqvFU4RLBQ6snwDp
fix: located Output-class variable in main PROGRAM wipes all POU variables on reopen (DOPE-445)
…or of openplc-web)

Byte-identical mirror of the openplc-web SoftMotion axis UI:
- SoftMotion icon (teal rotary-motion tile) marking a recognized CiA 402
  SoftMotion drive in the project tree.
- Cia402AxisTab: CODESYS-style axis config — enable toggle, increments↔units
  scaling, the CiA 402 object→IEC-address mapping table, and a real-time
  feedback panel. The device name is the axis name used in MC_*(Axis := …).
- project tree + explorer wire the `softMotionDrive` leaf lang when
  cia402.enabled; the EtherCAT device editor gains a "SoftMotion Axis" tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…channel mappings (mirror of openplc-web)

Byte-identical mirror of the openplc-web SoftMotion UX fixes:
1. Rename works for SoftMotion drives (isEthercatDevice now covers the
   softMotionDrive leaf lang).
2. Drive names are enforced to valid IEC identifiers — sanitized at add-time,
   validated on rename (isValidIecIdentifier), since the name is the axis
   variable in generated code.
3. Channel Mappings tab hidden for SoftMotion drives (PDO mappings are
   internal); editor defaults to the SoftMotion Axis tab.
4. SoftMotion Axis tab simplified (dropped the enable toggle and the
   application-code reference blurb).

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

Byte-identical mirror. The LSP now synthesizes a `VAR_GLOBAL <axis> : AXIS_REF_SM3`
document and injects the same axis VAR_EXTERNAL the compiler generates (via the
shared injectAxisExternals, now covering function blocks too), so editor code
that names a SoftMotion axis — `MC_Power(Axis := X_Axis)` — resolves instead of
flagging the axis as undeclared. Also fixes the compile gap where axis
references inside function blocks didn't get their VAR_EXTERNAL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…RNAL injection (mirror of openplc-web)

Byte-identical mirror. The LSP now surfaces each axis as a bare top-level
VAR_GLOBAL block (ambient global — resolves with no VAR_EXTERNAL), POUs are
serialised verbatim (no injected declarations shifting line numbers, so
go-to-definition stays correct), and go-to-definition on an axis redirects to
the owning drive's device editor. VAR_EXTERNAL injection remains compile-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olve (mirror of openplc-web)

Byte-identical mirror. The LSP now sends the project's configuration-level
globals as a CONFIGURATION VAR_GLOBAL doc (the level the compiler emits at, and
the only form strucpp matches a VAR_EXTERNAL against), so a POU's VAR_EXTERNAL
no longer falsely errors. Extracted a shared reconcileSyntheticDoc engine used
by all three synthesized docs (data types, resource globals, softmotion axes),
and go-to-definition on a user global opens the Resource editor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace all 25 bare useOpenPLCStore() whole-store subscriptions across the
graphical editor (ladder/FBD node atoms, RungBody, RungHeader, Rung,
LadderEditor, FBDBody, element-editor modals, autocompletes, useCopyPaste,
usePouSnapshot) with:

- narrowest-path selectors for render reads (pou-scoped find() selectors
  for flows, specific modal keys, primitives where possible)
- useOpenPLCStore.getState() at event time for handler-only reads, which
  removes re-render fan-out and stale-closure risk at once (the onDrop
  dep-array workarounds for freshly installed libraries are now moot)
- stable action-group selects

Store writes, flow persistence, and the compile pipeline input are
untouched; save-without-edit produces a zero source-control diff.

Live validation at 73 mounted rung instances (vs 2026-07-10 baseline):
typing is now ~2 commits/keystroke local to the focused node (baseline:
26-42 whole-editor commits/sec with ONE rung), idle is a flat 0 commits/5s
(baseline: sporadic 62-345 spikes), modal open/close no longer re-renders
the canvases (worst task 61ms vs a 470ms full-canvas pass).

Root cause C1 of DOPE-446. Fixes typing/idle/modal re-render storms;
rung-duplication scaling (C5) is DOPE-489, memo barriers are DOPE-488.

Mirror of the openplc-web commit (src/frontend byte-identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3FiQAY3P1gJDnGhuHbAab
JoaoGSP and others added 28 commits July 21, 2026 15:16
connection-types powerRail fold + dead deferred-resolve removal + EN
dedup guard; drop unused isOfType; remove write-only SyntheticVar origin
fields; block-library isRecord export + function-block-instance mapping;
pou-graphical shared isRecord + first-wins dedup note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNxKRqXuzVYSvfmUsvg2Ly
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RGT8nUsyY26HXFSuTBFLz
…ergences

fix(transpiler): byte-parity + library-agnostic block resolution (mirror of web #528)
…h-clones

perf(graphical-editor): remove edit hot-path deep clones + debounce flow write-back (DOPE-490)
…rites (DOPE-491)

- syncNodesWithVariables(FBD): batched updateNodes callback (one store
  commit per sweep) + optional rungId scoping; ladder add/drag-stop are
  rung-scoped, remove stays flow-scoped (block removal can delete
  variables referenced by other rungs); variable-table edits, project
  open and save relink keep the full sweep
- getFBDPouVariablesRungNodeAndEdges: WeakMap-cached per-rung lookups
  (node-by-id, edges-by-source/target) + variable name index — per-node
  render cost drops from O(nodes+edges) to O(1) amortized
- ladder/FBD debug styling: pure state computation with adjacency maps,
  deps narrowed to pouType + hasProgramInstance, content-stable guard so
  polls that don't change a rung keep styledNodes/styledEdges identity
- FBD mouse tracking: state -> refs read at paste time; no re-renders on
  canvas mouse travel
- debug poll: single setDebugValues commit per poll cycle (replaces
  setDebugBoolValues + setDebugNonBoolValues)

Mirror of openplc-web fix/dope-491-hot-path-scans

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RGT8nUsyY26HXFSuTBFLz
…ontent-stable

Addresses CodeRabbit review: reset the FBD flow's updated flag before
each updateNodes batch so the assertions actually prove the action's
behavior, and add unit tests locking in the useContentStable/mapsEqual
identity-stability contract.

Mirror of openplc-web fix/dope-491-hot-path-scans

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RGT8nUsyY26HXFSuTBFLz
perf(graphical-editor): scope per-edit scans + hot-path cleanups (DOPE-491)
…bers in LSP scope (#950)

* fix(graphical): bind dotted box refs as constants + resolve globals in LSP scope query

Byte-identical companion to the openplc-editor PR (shared frontend surface).

1. Dotted / non-identifier box entries (member access like
   some_global_complex.structureVar, typed literals like T#500ms, reserved
   words) are bound to the block verbatim as a constant/reference instead of
   being routed to createVariable, whose isLegalIdentifier check rejects them
   with an Illegal Variable Name toast and discards the entry. New variable
   NAMES still reject those characters. Applied to all four box-commit sites
   (fbd/ladder autocomplete + block).

2. serializePouScopeForQuery re-emits the POU's VAR_EXTERNAL variables as plain
   VAR in the throwaway scope-query doc, so strucpp resolves globals and their
   struct/array members without a matching VAR_GLOBAL — restoring LSP
   autocomplete + validation for global variables in LD and FBD.

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

* fix(graphical): accept LSP Field kind so struct members autocomplete/validate

Struct-typed variables (local or via a global VAR_EXTERNAL) never offered
their members in the LD/FBD variable boxes, e.g. `test_complex.structureBool`
for a BOOL box. Root cause: strucpp returns STRUCT members with
CompletionItemKind.Field (5), while graphical-scope only accepted Variable
(6) — the kind FUNCTION_BLOCK instance members come back as. So struct fields
were dropped by the kind filter before the type filter ran, in both the
completion and validation paths.

Accept Field (5) alongside Variable (6) via isValueCompletionKind at all
three call sites (completion, member-drill, type resolution). Verified live
in the browser: `<struct>.` now lists all fields in an ANY box and narrows to
the type-compatible field (structureBool) in a BOOL box.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: remove xml2st references + fix autocomplete box sizing

xml2st cleanup (the tool was retired; the in-process ST transpiler replaced it):
- Drop stale build/CI/config references (ci-build step name, ci-unit-tests
  comment, .gitattributes entry, devcontainer binary check).
- Rewrite every source/test comment that named xml2st to reference the ST
  transpiler / STruC++ as appropriate (no behavior change).
- Rename the exported PLCopen <addData> namespace
  openplc.org/xml2st/library-blocks -> openplc.org/library-blocks (nothing
  reads it back on import; xml2st was its only consumer). plc.xml generation
  itself is unchanged — it is the PLCopen-conversion / Export-as-XML flow.

Autocomplete box sizing:
- The graphical (LD/FBD) variable-box autocomplete had a fixed w-36 width, so
  long suggestions (e.g. struct member refs like some_global_complex.structureVar)
  were cropped. It now grows to fit content up to max-w-[16rem] and wraps long
  names (break-all) to multiple lines so the full text is always visible.

Byte-identical across the shared frontend surface (compare-surfaces: match, 0
diffs). tsc/lint/prettier clean; changed test suites pass; autocomplete sizing
verified live in the browser.

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

* fix(graphical): reserve scrollbar gutter in autocomplete so widest name never wraps

When the suggestion list overflows and the vertical scrollbar appears, it stole
horizontal space the content-based auto-size didn't reserve, wrapping the last
character of the widest name onto a new line. Add `scrollbar-gutter: stable` to
the scroll container so the reserved gutter is included in the auto-size.

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

* fix(graphical): stop autocomplete rows collapsing/overlapping in Safari

Safari-specific rendering of the LD/FBD variable-box dropdown:
- `w-max` pins the popover to the widest item's full single-line width (clamped
  by min/max). Safari's shrink-to-fit otherwise collapsed the width toward
  min-content when items use `break-all`, wrapping names unnecessarily.
- `shrink-0` on the item rows, the "Add variable"/new-block rows, and the
  variables container. Safari vertically shrinks flex-column children, so a
  wrapped name's second line overflowed its box and rendered on top of the
  "Add variable" button below. Keeping full height pushes siblings down instead.

Chrome was unaffected (sizes to max-content and doesn't shrink these rows);
verified no Chrome regression. Safari behavior to be confirmed by the reporter.

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

* fix(graphical): use break-words (not break-all) so Safari sizes the dropdown

Root cause of the Safari-only cropping: `word-break: break-all` collapses
WebKit's max-content computation, so the auto-sized dropdown came out too
narrow and wrapped names (even short ones), and the wrapped line overlapped
"Add variable". `overflow-wrap: break-word` (Tailwind `break-words`) does NOT
collapse max-content — it still breaks over-long names — so the popover sizes
to the full name (clamped to [9rem, 16rem]) in both Chrome and Safari. Dropped
the now-unneeded `w-max` (its interaction with min-width under WebKit was part
of the problem); Radix's `min-width: max-content` wrapper drives the sizing.
`shrink-0` rows retained so a genuinely long name that wraps still pushes
"Add variable" down instead of overlapping it.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…952)

Deleting a global variable that any POU declares as VAR_EXTERNAL was
silently refused by the store guard, but the global-variables editor
swallowed the failure response — the delete button just did nothing,
with no warning or message (odd UX).

Now the editor detects the referencing POUs up front and shows a
confirmation modal naming them. On confirm, deleteVariable is called
with force: true, which removes the global AND the matching
VAR_EXTERNAL declaration from every referencing POU. Graphical nodes
that still reference the deleted global go unresolved for the user to
fix (we only remove the external declarations, not diagram nodes).

- project slice: deleteVariable gains a force flag; without it, global
  deletion blocked by external refs returns the referencing POU names
  in data.referencingPous; with it, cascades the external-decl removal.
- global-variables editor: surfaces the block as a confirmation modal
  instead of a silent no-op.
- tests: assert referencingPous is reported (unforced) and that force
  cascades the removal across every referencing POU while retaining
  unrelated locals.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "User Management" screen under the project tree's Device folder,
shown only while connected to a runtime. Lets an admin list, create,
edit, and delete runtime accounts; a normal user can edit only its own
account.

- Shared frontend (editor + web, byte-identical): new `plc-user-management`
  editor variant, gated Device-tree leaf + Users icon, and a single-table
  screen with per-row edit/delete icons (OPC-UA style).
- Reusable RuntimeUserModal (create | edit | bootstrap). The first-user
  dialog is refactored to use it. Edit mode shows a masked password
  placeholder and only sends a password when the field is actually
  edited (dirty-tracked) — an untouched form never resets a password.
  Editing your own password requires the current password.
- RuntimePort gains listUsers / whoAmI / updateUser / deleteUser and a
  role on createUser; editor adapter + IPC channels implement them.
  create-user is sent unauthenticated for first-user bootstrap and
  authenticated (admin) afterwards.
- UI role-gates actions (admin sees create/delete + role selector); the
  runtime remains the real authorization boundary.
- Tests: adapter methods (100%), tabs factory, and the modal's
  dirty-password / current-password rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…c copy (DOPE-505)

The LSP transport built its connection from 'vscode-jsonrpc/browser' (direct
dep, 8.2.1) while message types (InitializeRequest, DidOpen...) came from
'vscode-languageserver-protocol' with its own nested 8.2.0. ParameterStructures
is compared by identity, so under a real pnpm node_modules layout the initialize
handshake threw "Unknown parameter structure byName" and crashed project load.
Import the transport through 'vscode-languageserver-protocol/browser' so the
connection and the message types share one vscode-jsonrpc copy in every layout
(npm, pnpm, bundled). Also clears the 14 dual-MessageConnection tsc errors.

Mirror of the openplc-web commit (shared src/frontend).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Zbz8hMi93XvA1RenC9bXN
…pc-single-copy

fix(lsp): resolve connection and message types from one vscode-jsonrpc copy (DOPE-505)
Same-major bump (11 minor releases) bringing measured perf wins on ladder/FBD
hot paths: skip-measurement rendering for predefined dimensions (12.8.5),
faster node/edge insertion (12.9), FlowRenderer re-render eliminations (12.10),
XYDrag only for draggable nodes + imperative viewport (12.11.2). Prerequisite
for the ladder single-instance-per-POU refactor. @xyflow/system 0.0.35 -> 0.0.79
rides along.

Two type-level breaks fixed in shared frontend:
- HandleProps.id widened to string|null in system 0.0.79 - pinned id?: string
  in CustomHandleProps (ladder + fbd handle.tsx) and typed buildHandle returns;
  our handles always carry explicit ids.
- OnNodeDrag event is now native MouseEvent|TouchEvent - drag handlers retyped
  in ladder rung body.tsx and fbd index.tsx; handleNodeDrag guards non-mouse
  events.

One behavior fix: removed nodeExtent from the ladder ReactFlowPanel. Node
positions belong to the ladder layout engine; xyflow >=12.9 no longer
recomputes drag-clamped positionAbsolute when the extent later grows, so a
mid-drag clamp against the stale extent froze nodes at wrong positions
(rendered corruption + "Maximum update depth" crash on drag-out-of-parallel).
12.0.1 masked this by unconditionally re-clamping all nodes on every extent
change. translateExtent (viewport lock) stays.

Mirror of the openplc-web commit (shared src/frontend byte-identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Zbz8hMi93XvA1RenC9bXN
Mirror of the openplc-web commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Zbz8hMi93XvA1RenC9bXN
…upgrade

chore(deps): upgrade @xyflow/react 12.0.1 → 12.11.2 (DOPE-492)
Addresses review feedback on the User Management feature:

1. First-user setup now stays connected — the bootstrap dialog no longer
   tears down the just-established session when it closes on success.
4. Authenticated create-user accepts the runtime's 201 (was treated as an
   error, so the dialog showed the raw response and stayed open).
5. The self-edit "Current password" field now renders last (after Confirm
   password) and only once the password is actually being changed, so it
   doesn't shove the field the user just clicked.
7. Gate the User Management tree leaf on runtime version ≥ v4.1.9
   (isUserManagementCapableRuntime); the connected runtime version is now
   stored in runtimeConnection and set on connect.

Also: edit-icon tooltip now reads "Edit user" (icon no longer swallows the
button title), "New User" label centered, and changing your own password
signs you out to force a fresh login with the new credentials.

Tests: version-gate helper, device-slice runtimeVersion, existing modal
and adapter suites updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bootstrap dialog was still disconnecting on the success close. Reading
runtimeConnection.connectionStatus in the close handler was stale: handleSubmit
sets it to 'connected', but RuntimeUserModal then calls onOpenChange(false)
synchronously before a re-render, so the handler saw the old 'connecting' value
and reverted to 'disconnected'.

Track success with a ref instead (set in handleSubmit, reset when the dialog
opens) so the close handler reliably distinguishes a successful connect from a
genuine cancel. Restores the pre-refactor behavior where the success path
closed via a controlled prop change that never triggered the cancel logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug 3: the PlusIcon strokes `inherit` with no stroke color set, so it
rendered invisibly — its empty box pushed the "New User" label off-center.
Give it `stroke-current` (white on the brand button) so it shows as a
proper "+ New User".

Bug 6 follow-up: changing your own password signs you out, but the User
Management tab stayed open and any action then crashed with
"users.map is not a function". Two guards:
- When not connected, render a neutral placeholder instead of the table
  and actions (which would hit the runtime unauthenticated).
- listUsers coerces a non-array payload to [] (the runtime returns an
  existence-only {"msg":"Users found"} object when the token is invalid),
  in the editor adapter and the screen, so the table can never receive a
  non-array.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tab atom maps fileDerivation.type to an icon and defaults to the IL
icon when the type isn't handled — so the User Management tab showed the
IL glyph. Add a 'user-management' case using the same UsersIcon as the
project-tree leaf.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: runtime User Management screen with RBAC
Bump APP_VERSION (the single source of truth the About dialog renders,
shared byte-for-byte with openplc-web) AND package.json.version together
so the desktop binary and the About dialog agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • development

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 98462cd3-0319-40cd-bc37-21d0c0c47865

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@thiagoralves
thiagoralves merged commit 789baea into main Jul 24, 2026
15 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.

3 participants