feat: add EtherCAT SDO/CoE support and device configuration UI#704
Conversation
- Add TypeScript types for EtherCAT discovery service API - Add IPC handlers for EtherCAT endpoints (interfaces, status, scan, test, validate) - Add EtherCATEditor component with network interface selector and device scan table - Enable EtherCAT protocol option in remote device creation - Integrate EtherCAT editor into workspace screen Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Redesigns the EtherCAT editor with a 3-tab architecture: - Repository: Upload and manage multiple ESI XML files with batch processing - Discovery: Scan network with automatic device-to-repository matching - Configured Devices: View and manage added devices with expandable details New components: - ESI repository table with expandable file/device views - Device browser modal for manual device selection - Discovered device table with match quality badges - Configured device rows with info panels New utilities: - ESI parser for XML file processing - Device matcher for scan-to-repository matching (exact/partial/none) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…of bytes - Add flex-1 and overflow-hidden to repository table container for proper scrolling - Display input/output channel counts instead of byte sizes in configured devices - Rename I/O column header to "Channels (In/Out)" for clarity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add ESIService to persist ESI XML files in the project's devices/esi/ directory. XMLs are now saved when uploaded and loaded automatically when the project is opened, ensuring they persist across sessions. - Create ESIService with load/save/delete operations for ESI files - Add IPC handlers for ESI repository management - Update EtherCAT editor to load repository from disk on mount - Update ESIRepository component to persist changes automatically - Store XML files with UUID filenames and maintain repository.json index Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add fast-xml-parser for high-performance XML parsing in main process - Implement two-level parsing: light summaries for listing, full on-demand - Upload files one at a time to prevent renderer memory crash with large batches - Cache device summaries in v2 repository index for instant reload - Add bulk clear repository operation (single IPC call instead of N) - Support automatic v1 to v2 repository migration - Handle multilingual ESI Name elements (prefer English LcId 1033) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parse errors no longer take over the screen. Shows a single compact banner with error count, expandable to a scrollable detail list. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix critical parseHexToNumber bug (parseInt with 0x prefix returned 0) - Fix Windows path separator in ESI service getEsiDir - Replace esiMigrateRepository with targeted v2 index update on delete - Add try/catch to IPC handlers for parseAndSaveFile and clearRepository - Display repository load errors with retry button - Add 100MB file size limit to ESI upload - Validate FMMU type against allowed values in both parsers - Fix TOCTOU race conditions in file operations (use ENOENT handling) - Add UUID validation on itemId to prevent path traversal - Fix PDO index forced to 0x0 when entries are empty - Fix parseHexValue regex to replace all #x occurrences - Remove dead _hexToNumber function from renderer parser Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add EtherCAT ESI repository and device configuration UI Redesigns the EtherCAT editor with a 3-tab architecture: - Repository: Upload and manage multiple ESI XML files with batch processing - Discovery: Scan network with automatic device-to-repository matching - Configured Devices: View and manage added devices with expandable details New components: - ESI repository table with expandable file/device views - Device browser modal for manual device selection - Discovered device table with match quality badges - Configured device rows with info panels New utilities: - ESI parser for XML file processing - Device matcher for scan-to-repository matching (exact/partial/none) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve repository table scroll and show channel counts instead of bytes - Add flex-1 and overflow-hidden to repository table container for proper scrolling - Display input/output channel counts instead of byte sizes in configured devices - Rename I/O column header to "Channels (In/Out)" for clarity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add ESI XML file persistence to project directory Add ESIService to persist ESI XML files in the project's devices/esi/ directory. XMLs are now saved when uploaded and loaded automatically when the project is opened, ensuring they persist across sessions. - Create ESIService with load/save/delete operations for ESI files - Add IPC handlers for ESI repository management - Update EtherCAT editor to load repository from disk on mount - Update ESIRepository component to persist changes automatically - Store XML files with UUID filenames and maintain repository.json index Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: optimize ESI XML parsing with lazy loading and sequential upload - Add fast-xml-parser for high-performance XML parsing in main process - Implement two-level parsing: light summaries for listing, full on-demand - Upload files one at a time to prevent renderer memory crash with large batches - Cache device summaries in v2 repository index for instant reload - Add bulk clear repository operation (single IPC call instead of N) - Support automatic v1 to v2 repository migration - Handle multilingual ESI Name elements (prefer English LcId 1033) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace individual error banners with collapsible summary Parse errors no longer take over the screen. Shows a single compact banner with error count, expandable to a scrollable detail list. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add loading feedback to Clear All button in ESI repository Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address code review issues in ESI subsystem - Fix critical parseHexToNumber bug (parseInt with 0x prefix returned 0) - Fix Windows path separator in ESI service getEsiDir - Replace esiMigrateRepository with targeted v2 index update on delete - Add try/catch to IPC handlers for parseAndSaveFile and clearRepository - Display repository load errors with retry button - Add 100MB file size limit to ESI upload - Validate FMMU type against allowed values in both parsers - Fix TOCTOU race conditions in file operations (use ENOENT handling) - Add UUID validation on itemId to prevent path traversal - Fix PDO index forced to 0x0 when entries are empty - Fix parseHexValue regex to replace all #x occurrences - Remove dead _hexToNumber function from renderer parser Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…rCAT devices Add channel-to-located-variable mapping table in the expanded view of each configured EtherCAT device. Channels extracted from ESI PDOs are auto-assigned IEC addresses (%IX, %QX, %IW, %QD, etc.) based on direction and data type. Users can manually edit any address. - Add EtherCATChannelMapping type and channelMappings field - Add generateIecLocation() and generateDefaultChannelMappings() utils - Create ChannelMappingTable component with direction filter and search - Load full device data lazily on row expand via esiLoadDeviceFull IPC - Fix stale eslint/tsc cache: disable eslint-webpack-plugin cache and clear tsbuildinfo on prestart Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
….json during compilation Add Zod schemas for EtherCAT configuration in PLCRemoteDeviceSchema, replace local useState with Zustand store read/write in the EtherCAT editor, and generate conf/ethercat.json in the compiler pipeline following the existing Modbus/S7Comm/OPC-UA pattern. Persistence to disk is handled automatically by the existing project-service. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…or runtime contract Persist complete channel metadata, PDO layouts, and slave type classification when adding devices, and rewrite the JSON generator to produce the exact contract expected by the OpenPLC runtime EtherCAT plugin. Old projects are backward-compatible and get enriched on first device expand. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow users to configure the EtherCAT master network interface and cycle time from the editor UI instead of relying on hardcoded defaults. When connected to runtime, a dropdown selector shows available interfaces; when offline, a text input allows manual entry. The config generator now produces one root entry per remote device with its own master settings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Include watchdog_timeout_cycles in the master config schema, UI panel, and JSON generator. The runtime uses this value to determine how many missed cycles trigger the watchdog (default: 3). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace parseInt + isNaN guard with Number() on change and onBlur clamping. This lets users backspace to clear the field before typing a new value, instead of the input snapping back immediately. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…global address uniqueness - Add optional alias field to EtherCATChannelMapping (matching Modbus pattern) - Make IEC Location column read-only (auto-generated), add editable Alias column - Generate globally unique IEC addresses across all protocols (Modbus + EtherCAT) - Modbus IO group generation now also checks EtherCAT addresses to avoid conflicts - EtherCAT aliases appear in variable location picker suggestions alongside Modbus aliases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove Index, Type, and Bits columns. Replace Name with a 1-based per-direction counter (#). Rename IEC Location to Address. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migrate EtherCAT scan from dedicated /api/discovery/ethercat/scan endpoint to the new generic /api/plugin-command infrastructure on the runtime side. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse CoE Dictionary from ESI XML (DataTypes + Objects with sub-item resolution) - Extract configurable RW parameters as SDOConfigurationEntry for startup - Auto-populate SDO defaults when enriching devices from ESI - Generate sdo_configurations in runtime JSON config - Add SdoParametersTable UI with type-aware inputs and reset-to-defaults - Auto-assign position for manually added devices - Add Zod schema for persistence (SDOConfigurationEntrySchema) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add real-time runtime status polling via plugin-command API, with a new RuntimeStatusPanel component that displays plugin state, per-slave status table, and cycle performance metrics when connected to the runtime. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Support REAL, LREAL, FLOAT, and DOUBLE types in the SDO parameters table with proper numeric ranges and step behavior. Parse SDO values as numbers (decimal, hex, float) instead of raw strings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reorganize the EtherCAT editor into three main tabs using Radix UI, mirroring the OPCUA plugin pattern: - Global Settings: network interface, cycle time, watchdog - Diagnostics: runtime status panel and device discovery/scan - Devices: ESI repository (collapsible) and configured devices with split-pane layout (device list left, detail panel right with sub-tabs for info, configuration, startup params, channel mappings) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ntime output Remove optionalSlave, checkRevisionNumber, and downloadPdoConfig fields that are no longer used by the runtime. Add complete RuntimeSlaveConfig (startup checks, addressing, timeouts, watchdog, distributed clocks) to the generated ethercat.json output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…oe-support Resolve merge conflicts keeping all SDO/CoE features (tabbed UI, runtime status monitoring, startup parameters) while incorporating updates from the epic branch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds end-to-end EtherCAT support: ESI parsing/persistence and IPC, EtherCAT discovery/runtime IPC, renderer EtherCAT editor and UI (repository, diagnostics, devices), new types/schemas, utilities to parse/match/enrich/generate runtime config, and compiler integration that emits conf/ethercat.json during compile/upload. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User (UI)
participant Renderer as Renderer (Browser)
participant Main as Main Process
participant Runtime as Runtime API
participant FS as File System
User->>Renderer: Click "Scan" (ip, jwt, iface)
Renderer->>Main: invoke "ethercat:scan"
Main->>Runtime: POST /api/discovery/scan
Runtime-->>Main: scan results
Main-->>Renderer: scan result { success, data }
User->>Renderer: Add selected device -> esiLoadDeviceFull(projectPath, id, idx)
Renderer->>Main: invoke "esi:load-device-full"
Main->>FS: read devices/esi/<id>.xml
FS-->>Main: xml content
Main->>Main: parseESIDeviceFull(xml, idx)
Main-->>Renderer: parsed ESIDevice
User->>Renderer: Save device config
Renderer->>Store: dispatch updateEthercatConfig(...)
Renderer->>Main: (on compile) invoke compile -> generateEthercatConfig(remoteDevices)
Main->>FS: write conf/ethercat.json
FS-->>Main: ack
Main-->>Renderer: compile/upload logs
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (7)
src/main/services/esi-service/index.ts (1)
86-100: Silent error suppression inloadRepositoryIndex.Non-ENOENT errors (e.g., JSON parse errors, permission issues) are logged but return
null, making them indistinguishable from a missing file. Consider returning an error state or throwing to allow callers to handle corrupted index files differently.Suggested improvement
async loadRepositoryIndex(projectPath: string): Promise<ESIRepositoryIndex | null> { + async loadRepositoryIndex( + projectPath: string + ): Promise<{ index: ESIRepositoryIndex | null; error?: string }> { const repoPath = this.getRepositoryPath(projectPath) try { const content = await promises.readFile(repoPath, 'utf-8') const index = JSON.parse(content) as ESIRepositoryIndex - return index + return { index } } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - return null + return { index: null } } console.error('Error reading ESI repository index:', error) - return null + return { index: null, error: error instanceof Error ? error.message : 'Unknown error' } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/services/esi-service/index.ts` around lines 86 - 100, The loadRepositoryIndex function currently treats all non-ENOENT errors as a missing index by logging and returning null, which hides parse/permission errors; update loadRepositoryIndex (and adjust callers if necessary) to rethrow non-ENOENT errors instead of returning null so callers can distinguish a missing file from a corrupted/unreadable index (retain the existing null return for ENOENT); reference this.getRepositoryPath, the loadRepositoryIndex method and the ESIRepositoryIndex type when making the change.src/utils/ethercat/esi-parser.ts (1)
248-268: Minor: Redundant case forbitLen === 1.The switch statement at line 254 handles
case 1which returns'BOOL', but this case is already covered by line 249 (bitLen === 1). This is not a bug but creates slightly redundant code.Optional simplification
// Handle BIT types if (esiType.startsWith('BIT') || bitLen === 1) { return 'BOOL' } // Infer from bit length switch (bitLen) { - case 1: - return 'BOOL' case 8: return 'BYTE'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/ethercat/esi-parser.ts` around lines 248 - 268, The code checks bitLen === 1 twice: once in the BIT-type guard (if (esiType.startsWith('BIT') || bitLen === 1) return 'BOOL') and again as case 1 in the switch; simplify by removing the redundant check — either drop "|| bitLen === 1" from the if so the switch handles bitLen cases, or remove the "case 1" branch from the switch and keep the existing early return; update the logic in esiType/bitLen handling in the function containing esiType and bitLen accordingly.src/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsx (2)
57-711: Consider extracting shared configuration form logic.The
ConfiguredDeviceRowcomponent duplicates significant portions of the configuration form fromDeviceDetailPanel. Consider extracting the configuration sections (Startup Checks, Addressing, Timeouts, Watchdog, Distributed Clocks) into reusable components to reduce duplication and ensure consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 57 - 711, ConfiguredDeviceRow duplicates the entire configuration form also present in DeviceDetailPanel; extract the shared UI/logic for "Startup Checks", "Addressing", "Timeouts", "Watchdog" and "Distributed Clocks (DC)" into a reusable component (e.g., DeviceConfigurationForm or SharedDeviceConfig) that accepts the device config, an update callback (use the existing updateConfig shape or onUpdateDevice), and helpers like parseNumericInput; move the InputWithRef/Checkbox wiring and disabled/validation behavior into that component and reuse it from both ConfiguredDeviceRow and DeviceDetailPanel, keeping the existing function names/props (updateConfig, parseNumericInput, config, InputWithRef, Checkbox) so callers simply pass config and a handler to apply partial updates.
103-159: Dependency array includes properties that won't trigger re-runs due to ref guard.Similar to
DeviceDetailPanel, theuseEffectincludes dependencies likedevice.channelInfo,device.rxPdos, etc., but thefullDeviceLoadedRef.currentguard prevents re-execution. The dependencies are technically correct for exhaustive-deps but could be misleading.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 103 - 159, The effect's dependency list includes properties (device.channelInfo, device.rxPdos, device.txPdos, etc.) that won’t cause re-runs because fullDeviceLoadedRef.current short-circuits loadFullDevice; simplify the deps to only the values that should actually trigger loading (e.g., isExpanded, projectPath, device.esiDeviceRef, device.channelMappings.length, onUpdateChannelMappings, onEnrichDevice, externalAddresses) by removing the individual device.* fields, or alternatively replace them with a single stable identifier (like device.esiDeviceRef or a memoized device object) so useEffect, fullDeviceLoadedRef, loadFullDevice and the dependency array reflect the real triggers for reloading.src/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsx (1)
108-161: Effect dependencies may cause unnecessary re-runs.The
useEffectdependency array includesdevice.channelMappings.length,device.channelInfo,device.rxPdos,device.txPdos, andexternalAddresses. SincefullDeviceLoadedRef.currentguards against re-execution, consider reducing the dependency array to only essential triggers or splitting the effect.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx around lines 108 - 161, The effect is over-dependent causing needless re-runs; keep only the true triggers for loading the full device and leave enrichment/update handlers as stable deps: replace the dependency array in the useEffect that defines loadFullDevice (the effect using fullDeviceLoadedRef.current, setIsLoadingChannels, window.bridge.esiLoadDeviceFull, pdoToChannels, generateDefaultChannelMappings, onUpdateChannelMappings, onEnrichDevice, enrichDeviceData) with a minimal list: [projectPath, device.esiDeviceRef, externalAddresses, onUpdateChannelMappings, onEnrichDevice]; remove derived/volatile properties like device.channelMappings.length, device.channelInfo, device.rxPdos, device.txPdos from the deps (or move logic that reacts to those properties into a separate effect if you need to respond to changes).src/types/ethercat/esi-types.ts (1)
595-612: Inline type definition fordeviceinScannedDeviceMatch.The
deviceproperty duplicates the structure ofEtherCATDevicefromindex.ts. Consider referencing the existing type or creating a shared base type to avoid duplication.Suggested change
+import type { EtherCATDevice } from './index' + export interface ScannedDeviceMatch { /** The scanned device from network discovery */ - device: { - position: number - name: string - vendor_id: number - product_code: number - revision: number - serial_number: number - state: string - input_bytes: number - output_bytes: number - } + device: Pick<EtherCATDevice, 'position' | 'name' | 'vendor_id' | 'product_code' | 'revision' | 'serial_number' | 'state' | 'input_bytes' | 'output_bytes'>Note: This would require careful handling of circular imports since
index.tsre-exports from this file.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/types/ethercat/esi-types.ts` around lines 595 - 612, ScannedDeviceMatch currently inlines a device object that duplicates the EtherCATDevice structure; replace the inline device property with a reference to the shared type (e.g., EtherCATDevice or a new EtherCATDeviceBase) to avoid duplication—edit the ScannedDeviceMatch interface to use that type for the device field and either import EtherCATDevice from its module or extract a small shared type into a new common types file to break potential circular imports; ensure ESIDeviceRef and DeviceMatch usages remain unchanged and update exports so index.ts re-exports the unified type without creating import cycles.src/main/modules/ipc/renderer.ts (1)
438-455: Prefer a shared type alias over repeating the repository index item shape.The inline nested
itemsstructure increases drift risk versus domain types already used in this file.♻️ Suggested refactor
+type ESIRepositoryIndexPayload = { + version: number + items: ESIRepositoryItem[] +} + esiLoadRepositoryIndex: ( projectPath: string, ): Promise<{ success: boolean - data?: { - version: number - items: Array<{ - id: string - filename: string - vendorId: string - vendorName: string - deviceCount: number - loadedAt: number - warnings?: string[] - }> - } | null + data?: ESIRepositoryIndexPayload | null error?: string }> => ipcRenderer.invoke('esi:load-repository-index', projectPath),As per coding guidelines,
src/**/*.{ts,tsx}should use strict TypeScript patterns and proper typing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/modules/ipc/renderer.ts` around lines 438 - 455, Replace the inline nested return type in esiLoadRepositoryIndex with a shared type alias: define a RepositoryIndexItem (or reuse existing domain type if present) for the objects in items and a RepositoryIndex (or RepositoryIndexResponse) for the overall data shape, then update the esiLoadRepositoryIndex signature to reference these aliases instead of repeating the inline structure; ensure the Promise return type uses the new types (e.g., Promise<{ success: boolean; data?: RepositoryIndex | null; error?: string }>) and import/export the types near other IPC/domain types in this file so the shape is centralized and reusable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/modules/ipc/main.ts`:
- Around line 596-600: The JSON body sent as postData for the ethercat scan
command omits scanRequest.timeout_ms, so pass timeout_ms through by adding
timeout_ms: scanRequest.timeout_ms into the params object when building postData
(the block that sets plugin: 'ethercat', command: 'scan', params: { interface:
scanRequest.interface }). Make the identical change in the other similar
postData creation (the second scan call around the other occurrence) so the
runtime receives and uses the UI-configured timeout.
In `@src/main/services/esi-service/index.ts`:
- Around line 396-442: parseAndSaveFile has a TOCTOU race between checking
existing filenames via loadRepositoryIndex and writing via saveXmlFile; make the
duplicate check-and-write atomic by either obtaining a project-level mutex/lock
around the sequence (check existingIndex, saveXmlFile, and
saveRepositoryIndexV2) or by using a write-idempotent approach (generate
deterministic item id from filename+project or allow duplicates and deduplicate
on read). Concretely, wrap the critical section in a lock (e.g.,
acquireLock(projectPath) before calling loadRepositoryIndex, saveXmlFile, and
saveRepositoryIndexV2, then releaseLock), or replace uuidv4() with a stable id
derived from filename and skip saving if the id already exists; update
parseAndSaveFile, loadRepositoryIndex, saveXmlFile, and saveRepositoryIndexV2
usage accordingly so the check and write are atomic.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/channel-mapping-table.tsx:
- Line 3: AliasCell seeds localAlias from the alias prop only on mount causing
stale input after parent updates; add an import for useEffect and inside
AliasCell add a useEffect that watches the alias prop and sets localAlias(alias)
whenever alias changes so the input stays in sync with prop updates and avoids
writing old values back on blur.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx:
- Around line 651-653: The call currently bypasses type checking by casting `{
coeObjects }` to `never`; instead pass a correctly typed ESIDevice (or a
properly constructed object matching ESIDevice) to enrichDeviceData and remove
the `as never` cast. Locate the button handler in configured-device-row.tsx
where onEnrichDevice is invoked and replace the unsafe cast with either the
actual device prop/variable (the same approach used in DeviceDetailPanel) or
build an ESIDevice object that includes coeObjects plus the required ESIDevice
fields before calling enrichDeviceData; ensure enrichDeviceData’s parameter type
is satisfied without using `as never`.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-devices.tsx:
- Around line 55-56: selectedDeviceId is only cleared on local removals so when
the parent-provided devices list changes externally the selection can point to a
non-existent device; update the component to watch the devices array (via a
useEffect) and call setSelectedDeviceId(null) whenever the current
selectedDeviceId is not found in the updated devices (also ensure the same check
is applied where selection logic currently exists around lines referencing
selection handling, e.g., the code block that removes devices and where
selectedDeviceId is used) so the selection is always cleared when the selected
device is removed externally.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-browser-modal.tsx:
- Around line 193-200: The device rows use a plain interactive div which is not
keyboard-accessible; update the element representing each row (the element using
key={`${repoItem.id}-${deviceIndex}`} and className with isSelected) to be
keyboard-focusable and activate on Enter/Space by either converting the div to a
semantic <button> or adding role="button", tabIndex={0}, and an onKeyDown
handler that calls handleSelectDevice(repoItem.id, deviceIndex) when Enter or
Space is pressed; also ensure any selected state (isSelected) is exposed via
aria-pressed or aria-selected to assistive tech.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx:
- Around line 614-616: The call enrichDeviceData({ coeObjects } as never)
unsafely casts a partial object to ESIDevice and causes runtime failures when
enrichDeviceData (and helpers persistPdos / deriveSlaveType) access rxPdo/txPdo;
fix by passing a valid ESIDevice or making enrichDeviceData accept
Partial<ESIDevice> and handle missing fields. Replace the cast in the onClick
handler with a properly constructed ESIDevice object containing the required
fields (at minimum the rxPdo/txPdo structures and any other non-optional
properties used by persistPdos/deriveSlaveType) or update enrichDeviceData (and
its callers) to accept Partial<ESIDevice> and add runtime guards/defaults for
rxPdo/txPdo before calling persistPdos/deriveSlaveType.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-channels-table.tsx:
- Around line 8-9: The header "select all" handler must pass the IDs of the
currently filtered rows instead of only a boolean: change the prop signature
onChannelSelectAll from (selected: boolean) => void to (selected: boolean,
channelIds: string[]) => void and update every call site in this component
(where you compute filteredChannels) to call onChannelSelectAll(selected,
Array.from(filteredChannels.keys()) or the equivalent filteredChannels.map(c =>
c.id)). Also update any parent/component usages of onChannelSelectAll to accept
the new second parameter; ensure the checkbox state still derives from
filteredChannels but the action operates only on the passed IDs.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-repository-table.tsx:
- Around line 111-116: The icon-only button using onToggleExpand and ArrowIcon
lacks an accessible name and does not expose expanded state; update the button
to include aria-expanded={isExpanded} and a clear aria-label that reflects the
state (e.g., "Expand row" vs "Collapse row") and, if the expandable panel has an
id, add aria-controls pointing to that id so screen readers can relate the
button to the content; ensure the expandable region component sets the matching
id.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-repository.tsx:
- Around line 128-130: The upload error list uses error.filename alone as the
React list key which can collide for duplicate or empty names; update the key in
the uploadErrors.map call to use a stable unique identifier (for example combine
error.filename with the map index or use a unique error.id if available) so the
<div key=...> for each error row is guaranteed unique; modify the JSX in the
mapping that renders uploadErrors to use that combined/unique value instead of
just error.filename.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-upload.tsx:
- Line 40: The filter for XML files only matches lowercase ".xml" so uppercase
extensions like "DEVICE.XML" get ignored; update the filter in the handler that
computes xmlFiles to perform a case-insensitive check (e.g., compare
file.name.toLowerCase().endsWith('.xml') or use a case-insensitive regex) so
filenames with any capitalization are accepted — locate the xmlFiles variable
and replace the current .endsWith('.xml') check with a case-insensitive
alternative.
- Around line 155-167: The drop-zone div uses onClick but lacks keyboard
semantics; add keyboard accessibility by making the div focusable and act like a
button: add tabIndex={0}, role="button", and an onKeyDown handler that invokes
handleClick when Enter or Space is pressed; also reflect the disabled state when
isProcessing by setting aria-disabled={isProcessing} and preserving
pointer-events and visual focus styles so keyboard users can trigger the file
picker the same way as mouse users (target the element with
onClick/onDragOver/onDragLeave/onDrop and the isProcessing prop/state).
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/sdo-parameters-table.tsx:
- Line 4: ValueCell is maintaining a stale localValue when upstream entry.value
(from sdoConfigurations) is updated (e.g., "Reset All to Defaults"); add a
useEffect inside the ValueCell component that watches entry.value and calls
setLocalValue(entry.value) to resync the input whenever entry.value changes,
ensuring the initial state is still derived from entry.value and avoiding stale
values on blur/submit.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/index.tsx:
- Around line 302-312: The effect watching isConnectedToRuntime is re-triggering
because checkServiceStatus and fetchInterfaces change identity each render; make
those callbacks stable (or call their logic inside the effect) so the effect
only depends on isConnectedToRuntime. Fix by memoizing checkServiceStatus and
fetchInterfaces with useCallback (ensuring their internal dependencies are
included) or move their implementations into the useEffect body (or expose
stable refs) and then change the dependency array to [isConnectedToRuntime] so
useEffect no longer loops; reference the existing useEffect and the
checkServiceStatus and fetchInterfaces functions to locate and update the code.
In `@src/utils/ethercat/sdo-config-defaults.ts`:
- Around line 50-57: The current code turns missing defaults into
empty-string/zero writes by using sub.defaultValue ?? '' (and similarly
obj.defaultValue ?? '') when building entries; change the logic in the SDO
defaults builder so you only add an entries.push for a parameter if a real
default exists (e.g., check if sub.defaultValue !== undefined/null before
constructing defaultValue and calling entries.push with index: obj.index,
subIndex: subIdx, value: defaultValue, defaultValue), and apply the same guard
to the other block that handles obj.defaultValue (lines ~67-73) so absent
defaults are skipped instead of emitted as startup writes.
---
Nitpick comments:
In `@src/main/modules/ipc/renderer.ts`:
- Around line 438-455: Replace the inline nested return type in
esiLoadRepositoryIndex with a shared type alias: define a RepositoryIndexItem
(or reuse existing domain type if present) for the objects in items and a
RepositoryIndex (or RepositoryIndexResponse) for the overall data shape, then
update the esiLoadRepositoryIndex signature to reference these aliases instead
of repeating the inline structure; ensure the Promise return type uses the new
types (e.g., Promise<{ success: boolean; data?: RepositoryIndex | null; error?:
string }>) and import/export the types near other IPC/domain types in this file
so the shape is centralized and reusable.
In `@src/main/services/esi-service/index.ts`:
- Around line 86-100: The loadRepositoryIndex function currently treats all
non-ENOENT errors as a missing index by logging and returning null, which hides
parse/permission errors; update loadRepositoryIndex (and adjust callers if
necessary) to rethrow non-ENOENT errors instead of returning null so callers can
distinguish a missing file from a corrupted/unreadable index (retain the
existing null return for ENOENT); reference this.getRepositoryPath, the
loadRepositoryIndex method and the ESIRepositoryIndex type when making the
change.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx:
- Around line 57-711: ConfiguredDeviceRow duplicates the entire configuration
form also present in DeviceDetailPanel; extract the shared UI/logic for "Startup
Checks", "Addressing", "Timeouts", "Watchdog" and "Distributed Clocks (DC)" into
a reusable component (e.g., DeviceConfigurationForm or SharedDeviceConfig) that
accepts the device config, an update callback (use the existing updateConfig
shape or onUpdateDevice), and helpers like parseNumericInput; move the
InputWithRef/Checkbox wiring and disabled/validation behavior into that
component and reuse it from both ConfiguredDeviceRow and DeviceDetailPanel,
keeping the existing function names/props (updateConfig, parseNumericInput,
config, InputWithRef, Checkbox) so callers simply pass config and a handler to
apply partial updates.
- Around line 103-159: The effect's dependency list includes properties
(device.channelInfo, device.rxPdos, device.txPdos, etc.) that won’t cause
re-runs because fullDeviceLoadedRef.current short-circuits loadFullDevice;
simplify the deps to only the values that should actually trigger loading (e.g.,
isExpanded, projectPath, device.esiDeviceRef, device.channelMappings.length,
onUpdateChannelMappings, onEnrichDevice, externalAddresses) by removing the
individual device.* fields, or alternatively replace them with a single stable
identifier (like device.esiDeviceRef or a memoized device object) so useEffect,
fullDeviceLoadedRef, loadFullDevice and the dependency array reflect the real
triggers for reloading.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx:
- Around line 108-161: The effect is over-dependent causing needless re-runs;
keep only the true triggers for loading the full device and leave
enrichment/update handlers as stable deps: replace the dependency array in the
useEffect that defines loadFullDevice (the effect using
fullDeviceLoadedRef.current, setIsLoadingChannels,
window.bridge.esiLoadDeviceFull, pdoToChannels, generateDefaultChannelMappings,
onUpdateChannelMappings, onEnrichDevice, enrichDeviceData) with a minimal list:
[projectPath, device.esiDeviceRef, externalAddresses, onUpdateChannelMappings,
onEnrichDevice]; remove derived/volatile properties like
device.channelMappings.length, device.channelInfo, device.rxPdos, device.txPdos
from the deps (or move logic that reacts to those properties into a separate
effect if you need to respond to changes).
In `@src/types/ethercat/esi-types.ts`:
- Around line 595-612: ScannedDeviceMatch currently inlines a device object that
duplicates the EtherCATDevice structure; replace the inline device property with
a reference to the shared type (e.g., EtherCATDevice or a new
EtherCATDeviceBase) to avoid duplication—edit the ScannedDeviceMatch interface
to use that type for the device field and either import EtherCATDevice from its
module or extract a small shared type into a new common types file to break
potential circular imports; ensure ESIDeviceRef and DeviceMatch usages remain
unchanged and update exports so index.ts re-exports the unified type without
creating import cycles.
In `@src/utils/ethercat/esi-parser.ts`:
- Around line 248-268: The code checks bitLen === 1 twice: once in the BIT-type
guard (if (esiType.startsWith('BIT') || bitLen === 1) return 'BOOL') and again
as case 1 in the switch; simplify by removing the redundant check — either drop
"|| bitLen === 1" from the if so the switch handles bitLen cases, or remove the
"case 1" branch from the switch and keep the existing early return; update the
logic in esiType/bitLen handling in the function containing esiType and bitLen
accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e34803c4-a8ab-43c1-958b-9fddf13efde9
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (42)
CLAUDE.mdconfigs/webpack/webpack.config.renderer.dev.tspackage.jsonsrc/main/modules/compiler/compiler-module.tssrc/main/modules/ipc/main.tssrc/main/modules/ipc/renderer.tssrc/main/services/esi-service/esi-parser-main.tssrc/main/services/esi-service/index.tssrc/renderer/components/_features/[workspace]/create-element/element-card/index.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/channel-mapping-table.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-devices.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-browser-modal.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-scan-table.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/devices-tab.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/diagnostics-tab.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/discovered-device-table.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-channels-table.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-device-info.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-parse-progress.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-repository-table.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-repository.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-upload.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/global-settings-tab.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/interface-selector.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/runtime-status-panel.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/sdo-parameters-table.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/index.tsxsrc/renderer/hooks/use-store-selectors.tssrc/renderer/screens/workspace-screen.tsxsrc/renderer/store/slices/project/slice.tssrc/renderer/store/slices/project/types.tssrc/types/PLC/open-plc.tssrc/types/ethercat/esi-types.tssrc/types/ethercat/index.tssrc/utils/ethercat/device-config-defaults.tssrc/utils/ethercat/device-matcher.tssrc/utils/ethercat/enrich-device-data.tssrc/utils/ethercat/esi-parser.tssrc/utils/ethercat/generate-ethercat-config.tssrc/utils/ethercat/sdo-config-defaults.ts
| <div | ||
| onClick={handleClick} | ||
| onDragOver={handleDragOver} | ||
| onDragLeave={handleDragLeave} | ||
| onDrop={handleDrop} | ||
| className={cn( | ||
| 'flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-6 transition-colors', | ||
| isDragging | ||
| ? 'bg-brand/10 dark:bg-brand/20 border-brand' | ||
| : 'border-neutral-300 hover:border-brand hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800/50', | ||
| isProcessing && 'pointer-events-none opacity-50', | ||
| )} | ||
| > |
There was a problem hiding this comment.
Add keyboard-accessible semantics to the upload drop zone.
Line 155 uses an interactive div (onClick) without keyboard activation/focus, so keyboard-only users can’t trigger file picking.
♿ Suggested fix
<div
+ role='button'
+ tabIndex={0}
onClick={handleClick}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ handleClick()
+ }
+ }}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div | |
| onClick={handleClick} | |
| onDragOver={handleDragOver} | |
| onDragLeave={handleDragLeave} | |
| onDrop={handleDrop} | |
| className={cn( | |
| 'flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-6 transition-colors', | |
| isDragging | |
| ? 'bg-brand/10 dark:bg-brand/20 border-brand' | |
| : 'border-neutral-300 hover:border-brand hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800/50', | |
| isProcessing && 'pointer-events-none opacity-50', | |
| )} | |
| > | |
| <div | |
| role='button' | |
| tabIndex={0} | |
| onClick={handleClick} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault() | |
| handleClick() | |
| } | |
| }} | |
| onDragOver={handleDragOver} | |
| onDragLeave={handleDragLeave} | |
| onDrop={handleDrop} | |
| className={cn( | |
| 'flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-6 transition-colors', | |
| isDragging | |
| ? 'bg-brand/10 dark:bg-brand/20 border-brand' | |
| : 'border-neutral-300 hover:border-brand hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800/50', | |
| isProcessing && 'pointer-events-none opacity-50', | |
| )} | |
| > |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-upload.tsx
around lines 155 - 167, The drop-zone div uses onClick but lacks keyboard
semantics; add keyboard accessibility by making the div focusable and act like a
button: add tabIndex={0}, role="button", and an onKeyDown handler that invokes
handleClick when Enter or Space is pressed; also reflect the disabled state when
isProcessing by setting aria-disabled={isProcessing} and preserving
pointer-events and visual focus styles so keyboard users can trigger the file
picker the same way as mouse users (target the element with
onClick/onDragOver/onDragLeave/onDrop and the isProcessing prop/state).
- Sync local state with props in AliasCell and ValueCell (useEffect) - Wrap AliasCell in React.memo to prevent unnecessary re-renders - Add index lock to ESI service to prevent race conditions in parseAndSaveFile, deleteRepositoryItemV2, and clearRepository - Fix clearRepository to only delete .xml files, not repository.json - Reset repositoryLoadedRef when projectPath changes - Remove selectedInterface from fetchInterfaces deps to prevent re-fetch cycle - Add Zod range constraints (int, min, max) to EtherCAT config schemas - Hoist parseNumericInput to module level in both detail components - Replace unsafe `as never` cast with direct extractDefaultSdoConfigurations call Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 13
♻️ Duplicate comments (1)
src/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsx (1)
614-616:⚠️ Potential issue | 🟠 MajorReplace the
as neverescape hatch here.This handler only has
coeObjects, butenrichDeviceData()also reads PDO data. The cast removes the compiler error and leaves a runtime crash path on click. Use the sameextractDefaultSdoConfigurations(coeObjects)path thatconfigured-device-row.tsxalready uses instead of callingenrichDeviceData()with a partial object.Suggested change
+import { extractDefaultSdoConfigurations } from '@root/utils/ethercat/sdo-config-defaults' @@ - <button - onClick={() => onEnrichDevice(enrichDeviceData({ coeObjects } as never))} + <button + onClick={() => onUpdateSdoConfigurations(extractDefaultSdoConfigurations(coeObjects))}As per coding guidelines,
src/**/*.{ts,tsx}: "Use strict TypeScript patterns, proper typing, and avoidanytypes".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx around lines 614 - 616, Replace the unsafe cast by passing a fully populated object built with extractDefaultSdoConfigurations: change the onClick handler that currently calls onEnrichDevice(enrichDeviceData({ coeObjects } as never)) to call onEnrichDevice(enrichDeviceData(extractDefaultSdoConfigurations(coeObjects))). This uses the same extractDefaultSdoConfigurations(coeObjects) path as configured-device-row.tsx to supply PDO/SDO data and avoids the runtime crash while keeping types correct for enrichDeviceData and onEnrichDevice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/services/esi-service/index.ts`:
- Around line 393-395: The call to saveRepositoryIndexV2 currently ignores its
non-throwing result; update the caller(s) to inspect the returned object (e.g.,
const res = await this.saveRepositoryIndexV2(projectPath, items)), check
res.success, and handle failures by logging the error details and either
throwing or returning an error result so the migration/upload does not report
success; additionally, if saveRepositoryIndexV2 fails, ensure you clean up any
partially written artifacts (e.g., remove the newly created XML from
parseAndSaveFile) to avoid orphaned files.
- Around line 246-260: The current saveRepositoryItem implementation trusts the
caller-supplied snapshot (existingItems) and builds updatedItems locally, which
can overwrite concurrent changes; instead, within the same locked/transactional
code path where you call saveXmlFile and saveRepositoryIndex, re-load the
authoritative index from storage (e.g., call the existing index-read method) and
merge/replace the item there before persisting so you never base updates on
stale external state; update the logic in saveRepositoryItem (and the similar
code path used at the other occurrence) to read the current index inside the
lock, apply the replacement of the item by id, then call saveRepositoryIndex
with that fresh merged list.
- Around line 361-395: migrateRepositoryToV2 currently reads and skips
unreadable XML files outside any lock and then overwrites the v2 index, risking
partial migrations; wrap the whole migration in withIndexLock so the index is
locked for the duration, and change the inner loop (calls to loadXmlFile and
parseESILight) to abort on the first load/parse failure by returning { success:
false, error: <descriptive message> } (or rethrow) instead of logging and
continuing; ensure no call to saveRepositoryIndexV2 occurs if any item failed,
and surface the specific filename and error in the returned error message so
callers can handle the atomic failure.
- Around line 97-110: The loadRepositoryIndex function currently returns null on
any read/parse failure which hides corrupt repository.json files; change its
error handling so only ENOENT returns null and all other errors (including
JSON.parse failures) are rethrown or propagated. In practice update the catch in
loadRepositoryIndex to detect (error as NodeJS.ErrnoException).code === 'ENOENT'
and return null only in that case, otherwise rethrow the error so callers can
surface/fail on a corrupted repository index instead of treating it as missing.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx:
- Around line 194-213: The row's click handler is on a non-focusable <tr>
(onSelect) and the expand icon button (ArrowIcon) is icon-only with no
accessible name or aria-expanded; make the selection and expansion operable via
keyboard by moving the selection handler from the <tr> to a focusable control
(e.g., a button or element with tabIndex and role) so onSelect is invoked from a
focusable element, and update the expand control (the button that calls
onToggleExpand and renders ArrowIcon) to include an accessible label (aria-label
or visually hidden text) and aria-expanded reflecting isExpanded; ensure the
focusable selection control and the expand button are distinct and both
keyboard-focusable.
- Around line 646-650: The fallback rendering for the CoE dictionary should only
appear after channel load completes and must distinguish between load error vs
empty result: update the conditional logic in configured-device-row.tsx (within
the component that references isLoadingChannels, channelLoadError and
coeObjects) so the block currently guarded by {!isLoadingChannels &&
!device.sdoConfigurations && coeObjects && coeObjects.length > 0} stays for the
"has objects" case, and add explicit branches when !isLoadingChannels &&
channelLoadError to render the load error, and when !isLoadingChannels &&
coeObjects && coeObjects.length === 0 to render the "no CoE dictionary"
empty-array message; ensure both places mentioned in the review (the block
around isLoadingChannels/coObjects and the similar block at the other
occurrence) are updated similarly.
- Around line 110-168: The effect can start multiple overlapping loads of
loadFullDevice causing races when responses apply state (setChannels,
setCoeObjects, onUpdateChannelMappings, onEnrichDevice); introduce an in-flight
guard token/ref (e.g., loadTokenRef) and set it before calling await
window.bridge.esiLoadDeviceFull(), capture the token in the closure, and after
each await verify the token still matches (ignoring the response if it doesn't)
so late responses won't overwrite newer state; ensure you still set
fullDeviceLoadedRef.current and clear/advance the token in finally so future
loads can run.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx:
- Around line 609-613: The current conditional around rendering CoE content uses
truthiness of coeObjects and doesn't account for load completion or errors;
update the conditions in the device detail panel to gate fallbacks on load
completion and error state by checking isLoadingChannels and channelLoadError
(e.g., only show empty/error UI when !isLoadingChannels and channelLoadError is
set), explicitly branch on coeObjects?.length === 0 to render the “no CoE
objects” empty state, and keep the positive branch for coeObjects?.length > 0
(while still skipping the whole block when device.sdoConfigurations is present).
- Around line 114-167: The effect's fullDeviceLoadedRef guard never resets when
the component receives a different device and the unsafe `as never` cast
bypasses typing; change the guard to track device identity (e.g. reset or
replace fullDeviceLoadedRef with a lastLoadedKey computed from
device.esiDeviceRef.repositoryItemId + device.esiDeviceRef.deviceIndex) so
loadFullDevice runs for new devices, and update the dependency list to include
that key; remove the unsafe cast in the onClick that calls
onEnrichDevice(enrichDeviceData(...)) and instead provide the correctly shaped
data (use the same pattern as configured-device-row.tsx — either call a helper
like extractDefaultSdoConfigurations(coeObjects) or construct the full device
shape before calling enrichDeviceData) so types are preserved and enrichment is
safe.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/sdo-parameters-table.tsx:
- Around line 68-83: handleBlur currently treats every non-BOOL SDO as a JS
Number and clamps via getDataTypeRange, which breaks text/custom values, allows
fractional input for integer types, and loses precision for 64-bit integers
(LINT/ULINT). Update handling to branch by entry.dataType
(SDOConfigurationEntry.dataType): keep BOOL as boolean; for integer types
enforce integer semantics (reject or round fractional input and validate with
integer regex or BigInt parsing for >2^53 values), for 64-bit integer types use
string/BigInt-aware parsing and preserve as string when out of safe JS number
range, for floating types use parseFloat/Number and clamp via getDataTypeRange,
and for string/custom types avoid numeric parsing entirely and pass localValue
through to onValueChange. Also ensure the rendered input type and validation
reflect the chosen branch (e.g., type="checkbox" for BOOL, text input for big
integers/strings, number/step for floats) and reference handleBlur,
getDataTypeRange, onValueChange, localValue, and
entry.{index,subIndex,dataType,bitLength} when making changes.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/index.tsx:
- Around line 357-402: handleAddSelectedFromScan is currently adding devices
from scan even if a device with the same physical position already exists in
configuredDevices; update the function to skip any scan match whose position is
already present in configuredDevices (or will be added in this batch) before
creating and pushing a new ConfiguredEtherCATDevice. In practice, check
configuredDevices.some(d => d.position === match.device.position) (and also
check newDevices for duplicates) and continue/skip when true so duplicates are
not appended, then proceed with enrichment and pushing only for positions not
already configured.
- Around line 269-305: The effect's async loadRepository can apply results for a
stale projectPath; fence or cancel the work by capturing the current projectPath
(or generating a unique loadId) at the start of loadRepository and verifying it
before calling setRepository, setRepositoryError, or setting
repositoryLoadedRef; alternatively use an AbortController-like ref (e.g.,
repositoryLoadTokenRef) that you update on each projectPath change and check
inside the async flow before applying results and after awaiting
esiLoadRepositoryLight/esiMigrateRepository to ignore responses for old projects
and clean up the token in the effect cleanup.
- Around line 206-212: The current refresh logic sets selectedInterface using
the previous value if present, which can leave selectedInterface referencing an
interface that no longer exists; in the result handling after calling
window.bridge.etherCATGetInterfaces, validate the previous selectedInterface
against the newly fetchedInterfaces: if fetchedInterfaces is empty, clear
selectedInterface; otherwise if the previous selectedInterface is absent from
fetchedInterfaces, setSelectedInterface to fetchedInterfaces[0].name; otherwise
keep the previous value. Use the existing state setters (setInterfaces,
setSelectedInterface) and the fetchedInterfaces array to perform this
revalidation so scanDevices never uses a stale selectedInterface.
---
Duplicate comments:
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx:
- Around line 614-616: Replace the unsafe cast by passing a fully populated
object built with extractDefaultSdoConfigurations: change the onClick handler
that currently calls onEnrichDevice(enrichDeviceData({ coeObjects } as never))
to call
onEnrichDevice(enrichDeviceData(extractDefaultSdoConfigurations(coeObjects))).
This uses the same extractDefaultSdoConfigurations(coeObjects) path as
configured-device-row.tsx to supply PDO/SDO data and avoids the runtime crash
while keeping types correct for enrichDeviceData and onEnrichDevice.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 04b2c4f0-88d6-4161-85bc-1db51555fac1
📒 Files selected for processing (7)
src/main/services/esi-service/index.tssrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/channel-mapping-table.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/sdo-parameters-table.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/index.tsxsrc/types/PLC/open-plc.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/renderer/components/_features/[workspace]/editor/device/ethercat/components/channel-mapping-table.tsx
- src/types/PLC/open-plc.ts
| async migrateRepositoryToV2( | ||
| projectPath: string, | ||
| ): Promise<{ success: boolean; items?: ESIRepositoryItemLight[]; error?: string }> { | ||
| try { | ||
| const index = await this.loadRepositoryIndex(projectPath) | ||
| if (!index) { | ||
| return { success: true, items: [] } | ||
| } | ||
|
|
||
| const items: ESIRepositoryItemLight[] = [] | ||
|
|
||
| for (const indexItem of index.items) { | ||
| try { | ||
| const xmlResult = await this.loadXmlFile(projectPath, indexItem.id) | ||
| if (xmlResult.success && xmlResult.content) { | ||
| const parseResult = parseESILight(xmlResult.content, indexItem.filename) | ||
| if (parseResult.success && parseResult.vendor && parseResult.devices) { | ||
| items.push({ | ||
| id: indexItem.id, | ||
| filename: indexItem.filename, | ||
| vendor: parseResult.vendor, | ||
| devices: parseResult.devices, | ||
| loadedAt: indexItem.loadedAt, | ||
| warnings: parseResult.warnings || indexItem.warnings, | ||
| }) | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.error(`Failed to migrate ESI file ${indexItem.filename}:`, err) | ||
| } | ||
| } | ||
|
|
||
| // Save as v2 | ||
| await this.saveRepositoryIndexV2(projectPath, items) | ||
|
|
There was a problem hiding this comment.
Make migration atomic and fail on the first unreadable XML.
This method bypasses withIndexLock, and it silently skips items whose XML cannot be loaded or reparsed before rewriting the v2 index. A concurrent write—or a single bad file—can therefore collapse the migrated catalog to a partial repository.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/services/esi-service/index.ts` around lines 361 - 395,
migrateRepositoryToV2 currently reads and skips unreadable XML files outside any
lock and then overwrites the v2 index, risking partial migrations; wrap the
whole migration in withIndexLock so the index is locked for the duration, and
change the inner loop (calls to loadXmlFile and parseESILight) to abort on the
first load/parse failure by returning { success: false, error: <descriptive
message> } (or rethrow) instead of logging and continuing; ensure no call to
saveRepositoryIndexV2 occurs if any item failed, and surface the specific
filename and error in the returned error message so callers can handle the
atomic failure.
| {!isLoadingChannels && !device.sdoConfigurations && coeObjects && coeObjects.length > 0 && ( | ||
| <div className='flex flex-col items-center gap-2 py-4'> | ||
| <p className='text-sm text-neutral-500 dark:text-neutral-400'> | ||
| CoE Object Dictionary available. Auto-configure startup parameters? | ||
| </p> |
There was a problem hiding this comment.
Only show the CoE empty state after a completed load.
This tab never renders channelLoadError, and the current conditions only distinguish coeObjects.length > 0 from !coeObjects. That makes failures fall through to the wrong empty message, and a successful load with coeObjects = [] renders nothing because [] is truthy. Gate the fallback on a completed load and handle coeObjects?.length === 0 explicitly.
Also applies to: 623-627
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx
around lines 609 - 613, The current conditional around rendering CoE content
uses truthiness of coeObjects and doesn't account for load completion or errors;
update the conditions in the device detail panel to gate fallbacks on load
completion and error state by checking isLoadingChannels and channelLoadError
(e.g., only show empty/error UI when !isLoadingChannels and channelLoadError is
set), explicitly branch on coeObjects?.length === 0 to render the “no CoE
objects” empty state, and keep the positive branch for coeObjects?.length > 0
(while still skipping the whole block when device.sdoConfigurations is present).
Critical: - Skip SDO entries with no defaultValue to prevent empty startup writes - Read index from disk in saveRepositoryItem/deleteRepositoryItem instead of trusting caller-supplied snapshots - Wrap migrateRepositoryToV2 with indexLock and check save result - Throw on corrupted repository.json instead of treating as missing Major: - Deduplicate scanned devices already configured (by position) - Cancel stale repository loads on projectPath change (mounted flag) - Replace `as never` cast in device-detail-panel with direct call - Validate selectedInterface against current interface list on refresh Minor: - Accept .XML extension (case-insensitive) in ESI upload - Clear stale selectedDeviceId when device list changes externally Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (5)
src/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-upload.tsx (1)
155-167:⚠️ Potential issue | 🟡 MinorAdd keyboard accessibility to the drop zone.
The drop zone uses
onClickbut lacks keyboard semantics (tabIndex,role="button",onKeyDown), preventing keyboard-only users from triggering the file picker. This was flagged in a previous review but appears unresolved.♿ Suggested fix for keyboard accessibility
<div + role='button' + tabIndex={0} onClick={handleClick} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleClick() + } + }} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} + aria-disabled={isProcessing} className={cn(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-upload.tsx around lines 155 - 167, The drop zone div currently only supports pointer interaction via onClick; add keyboard accessibility by making it focusable and operable: add tabIndex={0} and role="button" to the same div, and implement an onKeyDown handler that calls the existing handleClick when Enter or Space is pressed (ensure to prevent default for Space). Keep existing props (onDragOver, onDragLeave, onDrop) and respect isProcessing to disable keyboard activation when pointer-events are none; reference the div currently using handleClick, handleDragOver, handleDragLeave, handleDrop, isDragging, and isProcessing to locate where to add tabIndex, role, and onKeyDown.src/main/services/esi-service/index.ts (2)
481-485:⚠️ Potential issue | 🟠 MajorResult of
saveRepositoryIndexV2is not checked.If the index save fails after the XML is written (line 467), the function still returns
{ success: true, item }, leaving the XML file orphaned without an index entry. The past review comment flagging this issue appears unresolved.🐛 Proposed fix to check save result
// Append to v2 index const currentItems = await this.loadLightItemsFromIndex(projectPath) - await this.saveRepositoryIndexV2(projectPath, [...currentItems, item]) + const indexSaveResult = await this.saveRepositoryIndexV2(projectPath, [...currentItems, item]) + if (!indexSaveResult.success) { + // Clean up orphaned XML file + await this.deleteXmlFile(projectPath, itemId) + return { success: false, error: indexSaveResult.error ?? 'Failed to save repository index' } + } return { success: true, item }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/services/esi-service/index.ts` around lines 481 - 485, The code currently writes XML then appends to v2 index without checking saveRepositoryIndexV2's result, causing a success response even if the index save fails; update the calling code (the block that calls loadLightItemsFromIndex and saveRepositoryIndexV2) to capture the return value or throw from saveRepositoryIndexV2, check whether it succeeded, and only return { success: true, item } when the save indicates success—if saveRepositoryIndexV2 returns a boolean or result object, handle the failure path by rolling back or deleting the written XML (or returning { success: false, error }) and propagate an error; reference the functions saveRepositoryIndexV2 and loadLightItemsFromIndex to locate and modify the code.
404-423:⚠️ Potential issue | 🟡 MinorMigration silently drops items that fail to load or parse.
If an XML file cannot be loaded or parsed (lines 406-418), the error is logged but the item is excluded from the migrated v2 index. This can cause silent data loss if some XML files are corrupted or temporarily unreadable.
Consider failing the entire migration on the first error to avoid partial data loss, or at minimum returning the list of skipped items in the response so users are aware.
🛡️ Suggested fix to fail on first error
for (const indexItem of index.items) { - try { - const xmlResult = await this.loadXmlFile(projectPath, indexItem.id) - if (xmlResult.success && xmlResult.content) { - const parseResult = parseESILight(xmlResult.content, indexItem.filename) - if (parseResult.success && parseResult.vendor && parseResult.devices) { - items.push({ - id: indexItem.id, - filename: indexItem.filename, - vendor: parseResult.vendor, - devices: parseResult.devices, - loadedAt: indexItem.loadedAt, - warnings: parseResult.warnings || indexItem.warnings, - }) - } - } - } catch (err) { - console.error(`Failed to migrate ESI file ${indexItem.filename}:`, err) + const xmlResult = await this.loadXmlFile(projectPath, indexItem.id) + if (!xmlResult.success || !xmlResult.content) { + return { success: false, error: `Failed to load XML for ${indexItem.filename}: ${xmlResult.error}` } + } + const parseResult = parseESILight(xmlResult.content, indexItem.filename) + if (!parseResult.success || !parseResult.vendor || !parseResult.devices) { + return { success: false, error: `Failed to parse ${indexItem.filename}: ${parseResult.error}` } } + items.push({ + id: indexItem.id, + filename: indexItem.filename, + vendor: parseResult.vendor, + devices: parseResult.devices, + loadedAt: indexItem.loadedAt, + warnings: parseResult.warnings || indexItem.warnings, + }) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/services/esi-service/index.ts` around lines 404 - 423, The migration loop over index.items currently swallows failures (console.error) when loadXmlFile or parseESILight fail and silently omits items from the migrated v2 index; change the catch in that loop to abort the migration by rethrowing a descriptive Error (include indexItem.id and indexItem.filename and the original err) instead of logging, so callers of this method (the migration entrypoint) will fail the migration on the first load/parse error; alternatively, if you prefer non-fatal behavior, collect skipped item metadata into a skippedItems array and return it alongside the migrated items, but do not keep the current silent-continue behavior.src/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsx (2)
597-632:⚠️ Potential issue | 🟠 MajorShow the real CoE load state in the Startup Parameters tab.
Line 628 still treats load failures as “No CoE Object Dictionary available” because
channelLoadErroris never rendered here. A successful load withcoeObjects = []also falls through both branches and renders nothing. Mirror the channel-mapping tab’s error branch here and make the empty state explicit with!channelLoadError && (!coeObjects || coeObjects.length === 0).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx around lines 597 - 632, The Startup Parameters tab currently treats load failures and empty loads the same and omits rendering when coeObjects === [], so update the conditional branches around device.sdoConfigurations and coeObjects in the device-detail-panel component: add a branch that renders the channelLoadError (same as the channel-mapping tab) when channelLoadError is set, change the "no CoE Object Dictionary" empty-state condition to require !channelLoadError && (!coeObjects || coeObjects.length === 0), and ensure the Auto-configure button branch still uses coeObjects && coeObjects.length > 0 and calls onUpdateSdoConfigurations(extractDefaultSdoConfigurations(coeObjects)); this will surface load errors and explicitly show an empty-state when the load succeeded but returned no objects.
112-168:⚠️ Potential issue | 🟠 MajorReload the full-device cache when the backing ESI reference changes.
Line 116 is still a one-shot guard.
devices-tab.tsx:229-239remounts this panel wheneffectiveDevice.idchanges, but the same instance will skip reloading whenprojectPathordevice.esiDeviceRefchanges for that id, leaving stalechannels/coeObjectsin state. Because this effect also has no cleanup/token, a dependency change whileesiLoadDeviceFull(...)is in flight can still let an older response run Lines 130-145 against newer props. Please verify by switching the same configured device to a different ESI entry or project and confirming the panel refreshes instead of reusing the previous load.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx around lines 112 - 168, The one-shot guard fullDeviceLoadedRef prevents reloading when the backing ESI reference or project changes and allows stale async responses to overwrite newer props; remove or reset that guard when projectPath or device.esiDeviceRef changes and add a cancellation token/sequence check around the async loadFullDevice (the call to window.bridge.esiLoadDeviceFull) so only the latest response updates state. Specifically, reset fullDeviceLoadedRef.current (or remove it) when projectPath or device.esiDeviceRef changes, include a local requestId/abort flag checked before calling setChannels, setCoeObjects, onUpdateChannelMappings, and onEnrichDevice, and increment/flip that token on each effect run to ignore previous in-flight responses.
🧹 Nitpick comments (1)
src/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-upload.tsx (1)
168-168: Consider making the accept attribute case-insensitive.Some browsers treat the
acceptattribute case-sensitively, which may hide.XMLfiles in the file picker. WhileprocessFilescorrectly handles any case, adding both extensions improves the file picker UX.💡 Suggested enhancement
- <input ref={fileInputRef} type='file' accept='.xml' multiple onChange={handleFileSelect} className='hidden' /> + <input ref={fileInputRef} type='file' accept='.xml,.XML' multiple onChange={handleFileSelect} className='hidden' />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-upload.tsx at line 168, The file input's accept attribute is case-sensitive in some browsers and currently only lists '.xml'; update the JSX input element (the one using fileInputRef and onChange={handleFileSelect} in esi-upload.tsx) to include both lowercase and uppercase extensions (e.g., accept='.xml,.XML') so the file picker shows XML files regardless of casing; no changes needed to processFiles/handleFileSelect behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx:
- Around line 139-145: The current logic calls
onEnrichDevice(enrichDeviceData(result.device)) unconditionally when any
enrichment is needed, which overwrites device.sdoConfigurations (and slaveType)
even if they already exist as empty arrays; instead call
enrichDeviceData(result.device) into a local enriched variable and construct a
new device object that copies only missing fields: for example keep original
device.sdoConfigurations if it is defined (only take enriched.sdoConfigurations
when device.sdoConfigurations === undefined), and do the same for slaveType,
while still replacing channelInfo, rxPdos, txPdos when those are missing; then
pass that merged object to onEnrichDevice so you only backfill truly-missing
fields (referencing needsEnrichment, enrichDeviceData, onEnrichDevice,
device.sdoConfigurations, and slaveType).
---
Duplicate comments:
In `@src/main/services/esi-service/index.ts`:
- Around line 481-485: The code currently writes XML then appends to v2 index
without checking saveRepositoryIndexV2's result, causing a success response even
if the index save fails; update the calling code (the block that calls
loadLightItemsFromIndex and saveRepositoryIndexV2) to capture the return value
or throw from saveRepositoryIndexV2, check whether it succeeded, and only return
{ success: true, item } when the save indicates success—if saveRepositoryIndexV2
returns a boolean or result object, handle the failure path by rolling back or
deleting the written XML (or returning { success: false, error }) and propagate
an error; reference the functions saveRepositoryIndexV2 and
loadLightItemsFromIndex to locate and modify the code.
- Around line 404-423: The migration loop over index.items currently swallows
failures (console.error) when loadXmlFile or parseESILight fail and silently
omits items from the migrated v2 index; change the catch in that loop to abort
the migration by rethrowing a descriptive Error (include indexItem.id and
indexItem.filename and the original err) instead of logging, so callers of this
method (the migration entrypoint) will fail the migration on the first
load/parse error; alternatively, if you prefer non-fatal behavior, collect
skipped item metadata into a skippedItems array and return it alongside the
migrated items, but do not keep the current silent-continue behavior.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx:
- Around line 597-632: The Startup Parameters tab currently treats load failures
and empty loads the same and omits rendering when coeObjects === [], so update
the conditional branches around device.sdoConfigurations and coeObjects in the
device-detail-panel component: add a branch that renders the channelLoadError
(same as the channel-mapping tab) when channelLoadError is set, change the "no
CoE Object Dictionary" empty-state condition to require !channelLoadError &&
(!coeObjects || coeObjects.length === 0), and ensure the Auto-configure button
branch still uses coeObjects && coeObjects.length > 0 and calls
onUpdateSdoConfigurations(extractDefaultSdoConfigurations(coeObjects)); this
will surface load errors and explicitly show an empty-state when the load
succeeded but returned no objects.
- Around line 112-168: The one-shot guard fullDeviceLoadedRef prevents reloading
when the backing ESI reference or project changes and allows stale async
responses to overwrite newer props; remove or reset that guard when projectPath
or device.esiDeviceRef changes and add a cancellation token/sequence check
around the async loadFullDevice (the call to window.bridge.esiLoadDeviceFull) so
only the latest response updates state. Specifically, reset
fullDeviceLoadedRef.current (or remove it) when projectPath or
device.esiDeviceRef changes, include a local requestId/abort flag checked before
calling setChannels, setCoeObjects, onUpdateChannelMappings, and onEnrichDevice,
and increment/flip that token on each effect run to ignore previous in-flight
responses.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-upload.tsx:
- Around line 155-167: The drop zone div currently only supports pointer
interaction via onClick; add keyboard accessibility by making it focusable and
operable: add tabIndex={0} and role="button" to the same div, and implement an
onKeyDown handler that calls the existing handleClick when Enter or Space is
pressed (ensure to prevent default for Space). Keep existing props (onDragOver,
onDragLeave, onDrop) and respect isProcessing to disable keyboard activation
when pointer-events are none; reference the div currently using handleClick,
handleDragOver, handleDragLeave, handleDrop, isDragging, and isProcessing to
locate where to add tabIndex, role, and onKeyDown.
---
Nitpick comments:
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-upload.tsx:
- Line 168: The file input's accept attribute is case-sensitive in some browsers
and currently only lists '.xml'; update the JSX input element (the one using
fileInputRef and onChange={handleFileSelect} in esi-upload.tsx) to include both
lowercase and uppercase extensions (e.g., accept='.xml,.XML') so the file picker
shows XML files regardless of casing; no changes needed to
processFiles/handleFileSelect behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: fc4bfbbb-d8bb-4c1a-9535-399e904d100f
📒 Files selected for processing (6)
src/main/services/esi-service/index.tssrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-devices.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-upload.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/index.tsxsrc/utils/ethercat/sdo-config-defaults.ts
✅ Files skipped from review due to trivial changes (1)
- src/utils/ethercat/sdo-config-defaults.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-devices.tsx
- Pass timeout_ms through to runtime scan plugin-command params - Show channelLoadError in SDO tab instead of wrong empty state - Guard enrichment to never overwrite user-edited sdoConfigurations - Use text input for non-numeric SDO types (STRING, VISIBLE_STRING) - Use stable key (filename + index) for upload error rows Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (5)
src/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsx (3)
684-688:⚠️ Potential issue | 🟡 MinorSame empty
coeObjectsarray issue asdevice-detail-panel.tsx.If
coeObjects === [], neither the "auto-config" section (line 660 requireslength > 0) nor the "no CoE" fallback (line 684 checks!coeObjects) renders. Apply the same fix: check!coeObjects || coeObjects.length === 0.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 684 - 688, The conditional that shows the "No CoE Object Dictionary" fallback incorrectly treats an empty coeObjects array as present; update the JSX condition in the component (configured-device-row / the block that currently checks !isLoadingChannels && !channelLoadError && !device.sdoConfigurations && !coeObjects) to instead check !coeObjects || coeObjects.length === 0 so an empty array triggers the fallback; mirror the same check used by the "auto-config" section which expects coeObjects.length > 0.
107-175:⚠️ Potential issue | 🟠 MajorSame
fullDeviceLoadedRefissue asdevice-detail-panel.tsx.The ref never resets when the component receives a different device. Additionally, if dependencies like
externalAddresseschange while a load is in-flight, a second load can race with the first. Use a device-identity key in the ref and consider an in-flight cancellation token.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 107 - 175, fullDeviceLoadedRef currently never resets for a new device and in-flight loads can race with updated dependencies; change fullDeviceLoadedRef to store a device-identity key (e.g., combine device.esiDeviceRef.repositoryItemId and device.esiDeviceRef.deviceIndex or a device.id) and reset it when the identity changes, include that identity in the effect deps, and add an in-flight cancellation token (AbortController or a local "cancelled" flag checked after awaiting window.bridge.esiLoadDeviceFull in the loadFullDevice function) so that if externalAddresses or the device identity changes mid-load you bail out and don't apply stale setState or call onEnrichDevice/onUpdateChannelMappings.
201-240:⚠️ Potential issue | 🟠 MajorAccessibility: row selection and expand button lack keyboard support.
The
onClickon<tr>(line 202) is not keyboard-accessible. The expand button (lines 209-220) lacksaria-labelandaria-expanded. Screen reader users and keyboard navigators cannot interact with these controls effectively.🛠️ Suggested fixes
<tr - onClick={onSelect} + tabIndex={0} + role="row" + onClick={onSelect} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onSelect() + } + }} className={cn( 'cursor-pointer border-b border-neutral-200 dark:border-neutral-800', isSelected && 'bg-brand/10 dark:bg-brand/20', )} > <td className='px-2 py-2'> <button + aria-label={isExpanded ? 'Collapse device details' : 'Expand device details'} + aria-expanded={isExpanded} onClick={(e) => { e.stopPropagation() onToggleExpand() }} className='flex items-center justify-center' >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 201 - 240, The table row's click handler (onSelect) isn't keyboard-accessible and the expand button (onToggleExpand / ArrowIcon) lacks ARIA; make the row focusable and handle keyboard activation by adding tabIndex=0 to the <tr> and an onKeyDown that calls onSelect when Enter or Space is pressed (mirror the existing onClick behavior), and add accessible attributes to the expand button: include aria-expanded={isExpanded} and a descriptive aria-label (e.g., `Expand ${device.name}`) while keeping e.stopPropagation() so the button doesn't trigger row selection.src/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsx (2)
623-655:⚠️ Potential issue | 🟡 MinorEmpty
coeObjectsarray leaves the UI blank.If the ESI load succeeds but returns
coeObjects = [], the condition at line 627 (coeObjects.length > 0) fails and line 651 (!coeObjects) also fails since[]is truthy. The user sees nothing in the Startup Parameters section. Add an explicit branch for the empty-array case:🛠️ Suggested fix
{!isLoadingChannels && !channelLoadError && !device.sdoConfigurations && coeObjects && coeObjects.length > 0 && ( // ... auto-config button ... )} + {!isLoadingChannels && + !channelLoadError && + !device.sdoConfigurations && + coeObjects && + coeObjects.length === 0 && ( + <p className='py-4 text-center text-sm text-neutral-500 dark:text-neutral-400'> + No CoE Object Dictionary available for this device. + </p> + )} {!isLoadingChannels && !channelLoadError && !device.sdoConfigurations && !coeObjects && ( <p className='py-4 text-center text-sm text-neutral-500 dark:text-neutral-400'> No CoE Object Dictionary available for this device. </p> )}Or simplify by changing line 651's condition to
(!coeObjects || coeObjects.length === 0).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx around lines 623 - 655, The UI currently shows nothing when ESI returns an empty array because the first branch checks coeObjects.length > 0 and the "no CoE" message checks !coeObjects (which is false for []). Update the component logic: either add an explicit branch that checks Array.isArray(coeObjects) && coeObjects.length === 0 and render the "No CoE Object Dictionary available..." message, or change the existing final condition that uses !coeObjects to instead use (!coeObjects || coeObjects.length === 0) so the empty-array case is handled; reference the coeObjects variable and the surrounding conditions isLoadingChannels, channelLoadError, and device.sdoConfigurations when making this change.
115-178:⚠️ Potential issue | 🟠 MajorThe
fullDeviceLoadedRefguard doesn't reset when the device identity changes.If this component receives a different
deviceprop (differentrepositoryItemIdordeviceIndex), the ref remainstrueandloadFullDevicenever runs for the new device, leaving stale channel/CoE data displayed. Consider tracking device identity in the ref or resetting it based ondevice.esiDeviceRef:🛠️ Suggested approach
- const fullDeviceLoadedRef = useRef(false) + const lastLoadedKeyRef = useRef<string | null>(null) // Load full device data on mount useEffect(() => { - if (fullDeviceLoadedRef.current) return + const deviceKey = `${device.esiDeviceRef.repositoryItemId}-${device.esiDeviceRef.deviceIndex}` + if (lastLoadedKeyRef.current === deviceKey) return const loadFullDevice = async () => { // ... existing code ... - fullDeviceLoadedRef.current = true + lastLoadedKeyRef.current = deviceKey // ... }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx around lines 115 - 178, The guard fullDeviceLoadedRef prevents reload when the device prop changes; compare the current device identity (e.g., device.esiDeviceRef.repositoryItemId + ':' + device.esiDeviceRef.deviceIndex) to a stored lastDeviceIdRef inside the same useEffect and reset fullDeviceLoadedRef.current = false when they differ before the early return, so loadFullDevice runs for new devices; update lastDeviceIdRef with the new identity after resetting. Reference: fullDeviceLoadedRef, lastDeviceIdRef (create it), loadFullDevice, device.esiDeviceRef, and the existing useEffect dependencies.
🧹 Nitpick comments (2)
src/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-repository.tsx (1)
93-127: Set explicit button types in the error panel controls.Both buttons default to
type='submit'in form contexts; this can trigger unintended submits.🔧 Proposed fix
- <button onClick={() => setErrorsExpanded((prev) => !prev)} className='flex items-center gap-2'> + <button type='button' onClick={() => setErrorsExpanded((prev) => !prev)} className='flex items-center gap-2'> ... - <button + <button + type='button' onClick={() => setUploadErrors([])} className='text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300' >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-repository.tsx around lines 93 - 127, The two buttons in the error panel (the toggle that calls setErrorsExpanded and the "Dismiss" button that calls setUploadErrors([])) lack explicit type attributes and will act as type='submit' inside forms; update both button elements to include type="button" to prevent accidental form submissions, leaving their onClick handlers (setErrorsExpanded and setUploadErrors) unchanged and ensuring the existing className and SVG content remain intact.src/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsx (1)
25-57: Code duplication withdevice-detail-panel.tsx.The types (
EnrichDeviceData), constants (inputClassName,disabledInputClassName), and helper function (parseNumericInput) are duplicated fromdevice-detail-panel.tsx. Consider extracting these into a shared module (e.g.,@components/.../ethercat/utils/or a sharedconstants.ts) to reduce maintenance burden.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 25 - 57, The EnrichDeviceData type and duplicated UI constants/functions (EnrichDeviceData, inputClassName, disabledInputClassName, parseNumericInput) are repeated from device-detail-panel.tsx; extract these into a shared module (e.g., a new ethercat utils/constants file under components or a shared utils directory) and import them into configured-device-row.tsx and device-detail-panel.tsx, updating both files to reference the centralized EnrichDeviceData type and the shared inputClassName, disabledInputClassName, and parseNumericInput exports to remove duplication and keep a single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-repository.tsx:
- Around line 140-145: The table is receiving only isSaving and can render
prematurely; update the ESIRepositoryTable props to use the combined loading
state from the parent (e.g., replace the single isSaving prop with the combined
isLoading || isSaving or pass a new combinedLoading variable) so
ESIRepositoryTable receives the correct loading flag; locate the
ESIRepositoryTable usage and change the prop passed (keep existing handlers like
onRemoveItem/onClearAll intact).
---
Duplicate comments:
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx:
- Around line 684-688: The conditional that shows the "No CoE Object Dictionary"
fallback incorrectly treats an empty coeObjects array as present; update the JSX
condition in the component (configured-device-row / the block that currently
checks !isLoadingChannels && !channelLoadError && !device.sdoConfigurations &&
!coeObjects) to instead check !coeObjects || coeObjects.length === 0 so an empty
array triggers the fallback; mirror the same check used by the "auto-config"
section which expects coeObjects.length > 0.
- Around line 107-175: fullDeviceLoadedRef currently never resets for a new
device and in-flight loads can race with updated dependencies; change
fullDeviceLoadedRef to store a device-identity key (e.g., combine
device.esiDeviceRef.repositoryItemId and device.esiDeviceRef.deviceIndex or a
device.id) and reset it when the identity changes, include that identity in the
effect deps, and add an in-flight cancellation token (AbortController or a local
"cancelled" flag checked after awaiting window.bridge.esiLoadDeviceFull in the
loadFullDevice function) so that if externalAddresses or the device identity
changes mid-load you bail out and don't apply stale setState or call
onEnrichDevice/onUpdateChannelMappings.
- Around line 201-240: The table row's click handler (onSelect) isn't
keyboard-accessible and the expand button (onToggleExpand / ArrowIcon) lacks
ARIA; make the row focusable and handle keyboard activation by adding tabIndex=0
to the <tr> and an onKeyDown that calls onSelect when Enter or Space is pressed
(mirror the existing onClick behavior), and add accessible attributes to the
expand button: include aria-expanded={isExpanded} and a descriptive aria-label
(e.g., `Expand ${device.name}`) while keeping e.stopPropagation() so the button
doesn't trigger row selection.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/device-detail-panel.tsx:
- Around line 623-655: The UI currently shows nothing when ESI returns an empty
array because the first branch checks coeObjects.length > 0 and the "no CoE"
message checks !coeObjects (which is false for []). Update the component logic:
either add an explicit branch that checks Array.isArray(coeObjects) &&
coeObjects.length === 0 and render the "No CoE Object Dictionary available..."
message, or change the existing final condition that uses !coeObjects to instead
use (!coeObjects || coeObjects.length === 0) so the empty-array case is handled;
reference the coeObjects variable and the surrounding conditions
isLoadingChannels, channelLoadError, and device.sdoConfigurations when making
this change.
- Around line 115-178: The guard fullDeviceLoadedRef prevents reload when the
device prop changes; compare the current device identity (e.g.,
device.esiDeviceRef.repositoryItemId + ':' + device.esiDeviceRef.deviceIndex) to
a stored lastDeviceIdRef inside the same useEffect and reset
fullDeviceLoadedRef.current = false when they differ before the early return, so
loadFullDevice runs for new devices; update lastDeviceIdRef with the new
identity after resetting. Reference: fullDeviceLoadedRef, lastDeviceIdRef
(create it), loadFullDevice, device.esiDeviceRef, and the existing useEffect
dependencies.
---
Nitpick comments:
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx:
- Around line 25-57: The EnrichDeviceData type and duplicated UI
constants/functions (EnrichDeviceData, inputClassName, disabledInputClassName,
parseNumericInput) are repeated from device-detail-panel.tsx; extract these into
a shared module (e.g., a new ethercat utils/constants file under components or a
shared utils directory) and import them into configured-device-row.tsx and
device-detail-panel.tsx, updating both files to reference the centralized
EnrichDeviceData type and the shared inputClassName, disabledInputClassName, and
parseNumericInput exports to remove duplication and keep a single source of
truth.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-repository.tsx:
- Around line 93-127: The two buttons in the error panel (the toggle that calls
setErrorsExpanded and the "Dismiss" button that calls setUploadErrors([])) lack
explicit type attributes and will act as type='submit' inside forms; update both
button elements to include type="button" to prevent accidental form submissions,
leaving their onClick handlers (setErrorsExpanded and setUploadErrors) unchanged
and ensuring the existing className and SVG content remain intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b2239528-5d76-4050-89a8-500895f686ee
📒 Files selected for processing (5)
src/main/modules/ipc/main.tssrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-repository.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/sdo-parameters-table.tsx
✅ Files skipped from review due to trivial changes (1)
- src/main/modules/ipc/main.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/renderer/components/_features/[workspace]/editor/device/ethercat/components/sdo-parameters-table.tsx
| <ESIRepositoryTable | ||
| repository={repository} | ||
| onRemoveItem={handleRemoveItem} | ||
| onClearAll={handleClearAll} | ||
| isLoading={isSaving} | ||
| /> |
There was a problem hiding this comment.
Use the combined loading state for the table as well.
ESIRepositoryTable currently ignores parent isLoading (Line 144), so it can render non-loading/empty UI while data is still loading.
🔧 Proposed fix
<ESIRepositoryTable
repository={repository}
onRemoveItem={handleRemoveItem}
onClearAll={handleClearAll}
- isLoading={isSaving}
+ isLoading={isProcessing}
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <ESIRepositoryTable | |
| repository={repository} | |
| onRemoveItem={handleRemoveItem} | |
| onClearAll={handleClearAll} | |
| isLoading={isSaving} | |
| /> | |
| <ESIRepositoryTable | |
| repository={repository} | |
| onRemoveItem={handleRemoveItem} | |
| onClearAll={handleClearAll} | |
| isLoading={isProcessing} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-repository.tsx
around lines 140 - 145, The table is receiving only isSaving and can render
prematurely; update the ESIRepositoryTable props to use the combined loading
state from the parent (e.g., replace the single isSaving prop with the combined
isLoading || isSaving or pass a new combinedLoading variable) so
ESIRepositoryTable receives the correct loading flag; locate the
ESIRepositoryTable usage and change the prop passed (keep existing handlers like
onRemoveItem/onClearAll intact).
- Add makeRuntimeApiPostRequest with automatic token refresh on 401/403 - Refactor handleEtherCATScan, Test, Validate, and GetRuntimeStatus to use the shared POST helper instead of inline https.request - Guard loadFullDevice against concurrent in-flight loads - Pass combined isProcessing state to ESIRepositoryTable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsx (1)
201-220:⚠️ Potential issue | 🟠 MajorAdd keyboard accessibility for row selection and proper ARIA attributes for expand button.
The
onSelecthandler on<tr>is not keyboard-accessible since table rows aren't focusable by default. The expand button lacksaria-expandedand an accessible label. This blocks keyboard-only users from selecting devices or understanding the expand state.♿ Proposed accessibility fix
<tr - onClick={onSelect} + tabIndex={0} + role="button" + onClick={onSelect} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onSelect() + } + }} className={cn( 'cursor-pointer border-b border-neutral-200 dark:border-neutral-800', isSelected && 'bg-brand/10 dark:bg-brand/20', )} > <td className='px-2 py-2'> <button onClick={(e) => { e.stopPropagation() onToggleExpand() }} className='flex items-center justify-center' + aria-expanded={isExpanded} + aria-label={isExpanded ? 'Collapse device details' : 'Expand device details'} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 201 - 220, The table row with the onSelect handler is not keyboard-focusable and the expand button lacks ARIA attributes; make the <tr> (the row that calls onSelect) keyboard-accessible by adding tabIndex={0} and a keyDown handler that triggers onSelect for Enter and Space, or alternatively move the onSelect logic to a focusable element inside the row, and ensure the expand control (the button that calls onToggleExpand and renders ArrowIcon) includes aria-expanded={isExpanded} and a clear accessible name (aria-label or aria-labelledby) and, if there is an expandable region, aria-controls pointing to that region's id so assistive tech can detect the expanded state.
🧹 Nitpick comments (5)
src/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-repository.tsx (2)
4-5: Use configured path aliases instead of relative imports.Please switch these imports to the configured alias scheme to keep import resolution consistent across the renderer code.
♻️ Proposed change
-import { ESIRepositoryTable } from './esi-repository-table' -import { ESIUpload } from './esi-upload' +import { ESIRepositoryTable } from '@components/_features/[workspace]/editor/device/ethercat/components/esi-repository-table' +import { ESIUpload } from '@components/_features/[workspace]/editor/device/ethercat/components/esi-upload'As per coding guidelines, "Use path aliases as defined in tsconfig.json:
@root/*for./src/*,@process:main/*for./src/main/*,@process:renderer/*for./src/renderer/*,@components/*for./src/renderer/components/*,@utils/*for./src/utils/*,@shared/*for./src/shared/*,@hooks/*for./src/renderer/hooks/*".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-repository.tsx around lines 4 - 5, Replace the relative imports for ESIRepositoryTable and ESIUpload with the configured path-alias imports (use the `@components` alias which maps to src/renderer/components) so import statements reference the components via `@components/`... instead of './...'; update the import lines that reference the symbols ESIRepositoryTable and ESIUpload to use their aliased module paths (e.g. `@components/_features/`[workspace]/editor/device/ethercat/components/esi-repository-table and `@components/_features/`[workspace]/editor/device/ethercat/components/esi-upload).
46-49: Expose delete/clear failures in UI, not only in console logs.Line 46 and Line 66 only log failures, so users can’t tell when an action fails. Consider adding a small inline error banner/toast state for mutation failures.
Also applies to: 66-69
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-repository.tsx around lines 46 - 49, The console-only failure handling in the delete and clear mutation blocks (the console.error('Failed to delete ESI item:', result.error) and console.error('Error deleting ESI item:', err) lines, and the analogous clear failure logs around lines 66–69) should surface errors to users: add a local UI error state (e.g., deleteError/clearError or a generic mutationError) or invoke an existing toast/banner API from these handlers when result.error or catch triggers, set the error text from result.error or err, and update the component render to display a small inline banner/toast that clears on retry or after a timeout so users see failures instead of only console logs.src/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsx (3)
633-638: Consider using a dedicated spinner component instead of ArrowIcon.Using
ArrowIconwithanimate-spinas a loading indicator is semantically confusing—an arrow doesn't communicate "loading" to users (or assistive technologies). A dedicated spinner or loading component would provide clearer visual feedback.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 633 - 638, Replace the use of ArrowIcon inside the isLoadingChannels conditional with a proper loading/spinner component (e.g., Spinner or LoadingSpinner) to improve semantics and accessibility; update the JSX in the block that renders when isLoadingChannels to render the spinner component (or create one) instead of ArrowIcon, keep the existing wrapper classes ('flex items-center gap-2 py-4 text-sm ...') for layout, and ensure the spinner component exposes accessible attributes such as role="status" and an offscreen label or aria-live text like "Loading CoE data..." so assistive tech will announce the loading state.
53-57: Consider adding max validation toparseNumericInput.The function validates against
minbut notmax, while some inputs specify a max (e.g.,max={65535}on EtherCAT Address). While the HTML input provides browser-level validation, programmatically enforcingmaxwould ensure robustness against edge cases (e.g., direct value manipulation or copy-paste).🔧 Proposed enhancement
-const parseNumericInput = (value: string, min = 0): number | undefined => { +const parseNumericInput = (value: string, min = 0, max?: number): number | undefined => { const parsed = parseInt(value, 10) - if (isNaN(parsed) || parsed < min) return undefined + if (isNaN(parsed) || parsed < min || (max !== undefined && parsed > max)) return undefined return parsed }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 53 - 57, The parseNumericInput function only enforces a min but should also accept an optional max to guard against values above allowed ranges; change its signature to parseNumericInput(value: string, min = 0, max?: number) and, after parsing, return undefined if parsed is NaN, parsed < min, or (max !== undefined && parsed > max). Update any callers that rely on a specific upper bound (e.g., the EtherCAT Address input using max={65535}) to pass that max value into parseNumericInput so the value is programmatically validated as well as by the HTML input.
110-175: Race condition mitigation appears adequate, but verify the "load once" behavior is intentional.The
isLoadingChannelsguard prevents concurrent in-flight loads, andfullDeviceLoadedRef.currentensures the load happens only once per component instance. However, ifexternalAddresseschanges after the initial load completes (e.g., another device's mappings change), channel mappings won't be regenerated.If this "load once, never refresh" behavior is intentional for performance, consider adding a brief comment clarifying that design choice. If users expect mappings to update when external addresses change, you'd need a different approach.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx around lines 110 - 175, The effect currently prevents reloading after the first successful fetch via fullDeviceLoadedRef and isLoadingChannels, so changes to externalAddresses won't trigger regeneration of default mappings; either add a brief comment above the useEffect explaining the intentional "load once" behavior, or implement a minimal refresh: add a prevExternalAddressesRef and include a check in the effect to compare prevExternalAddressesRef.current with externalAddresses and, if different and device.channelMappings.length === 0, call onUpdateChannelMappings(generateDefaultChannelMappings(...)) (then update prevExternalAddressesRef.current), keeping existing guards (fullDeviceLoadedRef/isLoadingChannels) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx:
- Around line 201-220: The table row with the onSelect handler is not
keyboard-focusable and the expand button lacks ARIA attributes; make the <tr>
(the row that calls onSelect) keyboard-accessible by adding tabIndex={0} and a
keyDown handler that triggers onSelect for Enter and Space, or alternatively
move the onSelect logic to a focusable element inside the row, and ensure the
expand control (the button that calls onToggleExpand and renders ArrowIcon)
includes aria-expanded={isExpanded} and a clear accessible name (aria-label or
aria-labelledby) and, if there is an expandable region, aria-controls pointing
to that region's id so assistive tech can detect the expanded state.
---
Nitpick comments:
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/configured-device-row.tsx:
- Around line 633-638: Replace the use of ArrowIcon inside the isLoadingChannels
conditional with a proper loading/spinner component (e.g., Spinner or
LoadingSpinner) to improve semantics and accessibility; update the JSX in the
block that renders when isLoadingChannels to render the spinner component (or
create one) instead of ArrowIcon, keep the existing wrapper classes ('flex
items-center gap-2 py-4 text-sm ...') for layout, and ensure the spinner
component exposes accessible attributes such as role="status" and an offscreen
label or aria-live text like "Loading CoE data..." so assistive tech will
announce the loading state.
- Around line 53-57: The parseNumericInput function only enforces a min but
should also accept an optional max to guard against values above allowed ranges;
change its signature to parseNumericInput(value: string, min = 0, max?: number)
and, after parsing, return undefined if parsed is NaN, parsed < min, or (max !==
undefined && parsed > max). Update any callers that rely on a specific upper
bound (e.g., the EtherCAT Address input using max={65535}) to pass that max
value into parseNumericInput so the value is programmatically validated as well
as by the HTML input.
- Around line 110-175: The effect currently prevents reloading after the first
successful fetch via fullDeviceLoadedRef and isLoadingChannels, so changes to
externalAddresses won't trigger regeneration of default mappings; either add a
brief comment above the useEffect explaining the intentional "load once"
behavior, or implement a minimal refresh: add a prevExternalAddressesRef and
include a check in the effect to compare prevExternalAddressesRef.current with
externalAddresses and, if different and device.channelMappings.length === 0,
call onUpdateChannelMappings(generateDefaultChannelMappings(...)) (then update
prevExternalAddressesRef.current), keeping existing guards
(fullDeviceLoadedRef/isLoadingChannels) intact.
In
`@src/renderer/components/_features/`[workspace]/editor/device/ethercat/components/esi-repository.tsx:
- Around line 4-5: Replace the relative imports for ESIRepositoryTable and
ESIUpload with the configured path-alias imports (use the `@components` alias
which maps to src/renderer/components) so import statements reference the
components via `@components/`... instead of './...'; update the import lines that
reference the symbols ESIRepositoryTable and ESIUpload to use their aliased
module paths (e.g.
`@components/_features/`[workspace]/editor/device/ethercat/components/esi-repository-table
and
`@components/_features/`[workspace]/editor/device/ethercat/components/esi-upload).
- Around line 46-49: The console-only failure handling in the delete and clear
mutation blocks (the console.error('Failed to delete ESI item:', result.error)
and console.error('Error deleting ESI item:', err) lines, and the analogous
clear failure logs around lines 66–69) should surface errors to users: add a
local UI error state (e.g., deleteError/clearError or a generic mutationError)
or invoke an existing toast/banner API from these handlers when result.error or
catch triggers, set the error text from result.error or err, and update the
component render to display a small inline banner/toast that clears on retry or
after a timeout so users see failures instead of only console logs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 838bd78e-96de-4227-88ac-4cac0f2c5143
📒 Files selected for processing (3)
src/main/modules/ipc/main.tssrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsxsrc/renderer/components/_features/[workspace]/editor/device/ethercat/components/esi-repository.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/modules/ipc/main.ts
- device-browser-modal: replace interactive div with button for device rows, add aria-pressed state - esi-repository-table: add aria-expanded and aria-label to expand button - configured-device-row: add tabIndex and onKeyDown to table row for keyboard selection, add aria-expanded/aria-label to expand button - esi-upload: add role=button, tabIndex, onKeyDown and aria-label to drop zone for keyboard activation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…downloads) Resolve conflicts keeping all EtherCAT features while incorporating simulator support, file watcher IPC handlers, and binary download script from development. Combine prestart steps (rimraf tsbuildinfo + download-binaries). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| @@ -0,0 +1,698 @@ | |||
| import * as Tabs from '@radix-ui/react-tabs' | |||
There was a problem hiding this comment.
~450+ lines of near-identical code duplicated between configured-device-row.tsx and this file: same EnrichDeviceData type, inputClassName/disabledInputClassName constants, parseNumericInput utility, useEffect for loadFullDevice, handleAliasChange/updateConfig callbacks, and the entire configuration form (Startup Checks, Addressing, Timeouts, Watchdog, Distributed Clocks SYNC0/SYNC1, SDO Parameters, Channel Mappings). The only difference is layout: table rows vs. tabbed panel.
Suggestion: Extract a shared DeviceConfigurationForm component and a useDeviceConfiguration hook to eliminate ~400-500 lines of duplication and prevent the two views from drifting out of sync.
| device: ConfiguredEtherCATDevice | ||
| repository: ESIRepositoryItemLight[] | ||
| isExpanded: boolean | ||
| onToggleExpand: () => void |
There was a problem hiding this comment.
This EnrichDeviceData type is defined identically in 4 separate files: configured-device-row.tsx, device-detail-panel.tsx, configured-devices.tsx, and devices-tab.tsx.
Define it once in a shared location (e.g. src/utils/ethercat/enrich-device-data.ts alongside the function that returns this shape, or src/types/ethercat/esi-types.ts) and import it in all four components.
| @@ -0,0 +1,612 @@ | |||
| /** | |||
There was a problem hiding this comment.
Two complete ESI XML parsers with duplicated logic: this file (renderer, DOMParser) and esi-parser-main.ts (main process, fast-xml-parser). Both share identical parseHexValue, PDO entry parsing, FMMU/SM parsing, and device type extraction code.
Since all ESI parsing now routes through IPC to the main process parser, the renderer-side parseESI() function may be dead code. Its remaining exports (pdoToChannels, generateDefaultChannelMappings, generateIecLocation) operate on already-parsed ESIDevice objects, not XML — they're channel utilities, not parsers.
Suggestions:
- Verify if
parseESI()is still called anywhere; remove it if dead code - Move shared helpers like
parseHexValueto a common utility - Rename this file to
esi-channel-utils.tsto reflect its actual remaining purpose
| channelInfo: device.channelInfo, | ||
| rxPdos: device.rxPdos, | ||
| txPdos: device.txPdos, | ||
| slaveType: device.slaveType ?? '', |
There was a problem hiding this comment.
delete enriched.sdoConfigurations mutates the return value of enrichDeviceData(). While it returns a fresh object today (so no shared-reference issues), this breaks if the function is ever memoized or cached, and is a code smell.
Prefer destructuring:
const { sdoConfigurations, ...rest } = enrichDeviceData(result.device)
onEnrichDevice(device.sdoConfigurations !== undefined ? rest : { ...rest, sdoConfigurations })Same pattern appears in device-detail-panel.tsx.
| - Node.js >= 20.x < 24 | ||
| - npm >= 10.x | ||
| - Supported platforms: macOS, Windows, Linux (x64 & ARM64) | ||
|
|
There was a problem hiding this comment.
This +74 line "Architecture Survey — Autonomy Logic" section is unrelated to the EtherCAT feature. It's also written in Portuguese while the rest of the codebase uses English. Consider removing it from this PR and adding it in a separate documentation commit.
| onUpdateChannelMappings(updated) | ||
| }, | ||
| [device.channelMappings, onUpdateChannelMappings], | ||
| ) |
There was a problem hiding this comment.
This useEffect dependency array includes unstable references: externalAddresses (a new Set recomputed on every relevant re-render), callback props, and device.channelMappings.length. The fullDeviceLoadedRef guard prevents the async function from re-running, but the effect still re-enters on every dep change and hits the early return.
The fullDeviceLoadedRef pattern is fighting against React's dependency model — if any parent ever changes callback identity, the ref guard silently prevents re-loading even when it should.
Consider extracting this into a useDeviceConfiguration hook with dependencies limited to projectPath and device.esiDeviceRef, managing all loading/enrichment state internally. Same pattern exists in device-detail-panel.tsx.
|
private wrapServiceCall<T>(fn: () => Promise<T>): Promise<T | { success: false; error: string }> {
return fn().catch((error) => ({ success: false, error: String(error) }))
}This would reduce the 13 handlers to ~40 lines. Also, |
… code Extract shared useDeviceConfiguration hook and DeviceConfigurationForm component to eliminate ~1500 lines of duplication between configured-device-row and device-detail-panel. Also: centralize EnrichDeviceData type, fix EtherCATSlaveConfig name collision, replace delete mutation with destructuring, remove dead renderer-side ESI parser, reduce IPC handler boilerplate with wrapServiceCall helper, and remove unrelated Architecture Survey from CLAUDE.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
fast-xml-parser%IX,%QW,%QD, etc.) with editable aliases and global address uniquenessethercat.jsonduring compilation with full slave configs, runtime status monitoring panel, plugin-command API for device scanningTest plan
conf/ethercat.jsongenerated with correct structurenpm run start:devbuilds without errors🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Chores
Documentation