Skip to content

refactor(ai): sync shared surfaces for AI chat enhancements#732

Merged
dcoutinho1328 merged 6 commits into
developmentfrom
refactor/ai-chat-pr353
Apr 22, 2026
Merged

refactor(ai): sync shared surfaces for AI chat enhancements#732
dcoutinho1328 merged 6 commits into
developmentfrom
refactor/ai-chat-pr353

Conversation

@dcoutinho1328
Copy link
Copy Markdown
Collaborator

@dcoutinho1328 dcoutinho1328 commented Apr 16, 2026

Summary

  • Sync byte-identical surfaces from openplc-web refactor/ai-chat-pr353
  • Store: project-scoped flat messages[] replacing per-POU conversations[], add isAgenticLoopRunning
  • Monaco: replace DiffEditor with event-based model sync (ai-pou-updated)
  • Architecture: add adapter-components layer with frontend-level permissions
  • CSS: add AI diff review decoration styles (dormant in editor — no AI triggers them)

Affected Surfaces

  • Shared (byte-identical): frontend/store/slices/ai/, frontend/store/__tests__/, frontend/components/.../monaco/, backend/shared/styles/, __architecture__/
  • Editor-specific: none
  • Web-specific: none (web-only changes are in Autonomy-Logic/openplc-web#361)

Validation

  • Architecture validation passes
  • AI slice tests pass (41/41)
  • Byte-identical surfaces pass (compare_repos.py — 837 files, all identical)

Cross-repo

  • Matching PR: Autonomy-Logic/openplc-web#361

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Interactive AI diff-review in the editor with per-hunk "Keep" and "Undo" buttons, view zones for removed lines, and global accept/reject actions.
    • AI messaging simplified to a single flat message list, new "agentic loop" flag, and increased capacity from 20 → 50 messages.
  • Bug Fixes

    • Visual styling for AI-reviewed code regions (added/removed highlights and gutters).
    • More robust editor synchronization when AI updates code.
  • Chores

    • Removed the editor AI status badge.

Sync byte-identical surfaces from openplc-web refactor/ai-chat-pr353:

- Store: project-scoped flat messages[] replacing per-POU conversations,
  add isAgenticLoopRunning, increase MAX_MESSAGES to 50
- Monaco: replace DiffEditor with ai-pou-updated event handler
- Architecture: add adapter-components layer with frontend-level permissions
- CSS: add AI diff review decoration styles
- Tests: update AI slice tests for new schema

All AI feature logic remains web-only (in adapters). These shared surface
changes keep store types, components, and validation in sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 235887d9-531a-4c79-a486-37eeb4606de3

📥 Commits

Reviewing files that changed from the base of the PR and between 1efb52c and fa62720.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Walkthrough

Adds an adapter-components architecture layer; introduces AI-diff CSS and frontend diff-review utilities; replaces Monaco DiffEditor flow with decoration-driven per-hunk review, new diff compute/apply logic, and updated AI event handling; migrates AI store from conversation-scoped to a flat messages model and updates related types, slice, and tests.

Changes

Cohort / File(s) Summary
Architecture & Validation
src/__architecture__/validate.ts
Added adapter-components LayerName and LAYER_RULES entry; updated getLayer() to classify middleware/adapters/<adapter>/components/ as adapter-components and narrowed adapters naming.
Styling
src/backend/shared/styles/globals.css
Added AI diff visualization classes: .ai-diff-added, .ai-diff-added-gutter, .ai-diff-removed-zone with dark-mode variants.
Editor — Monaco integration
src/frontend/components/_features/[workspace]/editor/monaco/index.tsx, src/frontend/components/_features/[workspace]/editor/monaco/ai-diff-review.ts, src/frontend/components/_features/[workspace]/editor/monaco/ai-status-indicator.tsx
Reworked editor AI handling to use ai-pou-updated events, in-place executeEdits, and per-hunk diff state; added renderDiffReview (decorations, view zones, Keep/Undo buttons); removed DiffEditor-based flow and deleted AIStatusIndicator.
Diff utilities
src/frontend/utils/ai-diff-review.ts
Added computeHunks and applyAcceptedHunks plus DiffHunk types to compute/apply line-based hunks and translate accepted/rejected hunks into final body.
AI Store — types, slice, exports, tests
src/frontend/store/slices/ai/types.ts, src/frontend/store/slices/ai/slice.ts, src/frontend/store/slices/ai/index.ts, src/frontend/store/__tests__/ai-slice.test.ts
Replaced conversation-centric state (conversations, activeConversationPou) with flat messages and activeEditorPou; added isAgenticLoopRunning; increased MAX_CONVERSATION_MESSAGES 20→50; updated action signatures to remove POU params and added setAgenticLoopRunning; tests updated accordingly.
Dependencies
package.json
Added diff and @types/diff; bumped dompurify to ^3.4.0.

Sequence Diagram(s)

sequenceDiagram
  participant AI as AI Service
  participant Editor as Monaco Editor
  participant Utils as Diff Utils
  participant UI as Diff Review UI
  participant Store as Frontend Store

  AI->>Editor: emit `ai-pou-updated` (oldBody?, body)
  alt no oldBody
    Editor->>Editor: executeEdits('ai-tool-update', body)
    Editor->>Store: update localText / updatePou(body)
  else oldBody present
    Editor->>Utils: computeHunks(oldBody, body)
    Utils-->>Editor: hunks[]
    Editor->>UI: renderDiffReview(editor, hunks, onKeep, onUndo)
    UI-->>Editor: add decorations, viewZones, Keep/Undo buttons
    Note over UI,Editor: User clicks Keep/Undo per hunk
    UI->>Editor: onKeep(hunkId) / onUndo(hunkId)
    Editor->>Store: update acceptedHunks set
    Editor->>Utils: applyAcceptedHunks(oldBody, body, hunks, acceptedIds)
    Utils-->>Editor: newBody
    Editor->>Editor: executeEdits('ai-tool-update', newBody)
    Editor->>Store: updatePou(newBody)
  end
  UI->>UI: cleanup when no pending hunks remain
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • JoaoGSP
  • thiagoralves

Poem

🐰
I hopped through hunks with whiskered cheer,
Turned many conversations into messages clear.
Buttons to keep, buttons to mend,
New layers sprout as diffs ascend. 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective: syncing shared surfaces for AI chat enhancements. It is concise, specific, and reflects the primary purpose of the changeset.
Description check ✅ Passed The description covers the key changes comprehensively with clear sections (Summary, Affected Surfaces, Validation, Cross-repo), but lacks most required DOD checklist items and missing issue/Jira references as specified in the template.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/ai-chat-pr353

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/__architecture__/validate.ts (2)

67-70: Nit: adapters layer name is narrower than what getLayer() actually classifies.

The updated label claims this layer covers middleware/adapters/**/services/, middleware/adapters/*.ts, but the classifier on line 152 (rel.startsWith('middleware/adapters/')) still catches any non-components/ path under an adapter (e.g., middleware/adapters/<adapter>/utils/foo.ts, top-level adapter files, etc.), not only services/. When a violation is reported for such a file, the error message will misrepresent where it lives. Consider either tightening getLayer() to only match services/ + top-level adapter files, or broadening the name back to reflect the actual catch-all scope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__architecture__/validate.ts` around lines 67 - 70, The adapters layer
label is narrower than the current classifier: getLayer() uses
rel.startsWith('middleware/adapters/') and therefore matches any file under that
directory (not just services/), so either (A) broaden the adapters.name to
accurately describe the catch-all scope or (B) tighten getLayer() to only
classify service files and top-level adapter modules by changing the condition
that uses rel.startsWith('middleware/adapters/') to require either a
'/services/' segment or a top-level adapter filename pattern (e.g., match
'middleware/adapters/*.ts'); update the adapters.name or the getLayer() matching
logic accordingly so the reported layer and classifier stay consistent.

71-86: LGTM on the new adapter-components layer rule.

Ordering in getLayer() (line 151 before line 152) correctly routes middleware/adapters/<adapter>/components/** into this layer before the broader adapters fallback. The allowedDeps set mirrors the components layer plus adapters, which is consistent with an adapter-level UI layer composing frontend components and calling into adapter services.

Two small, optional observations:

  • Listing 'adapter-components' in its own allowedDeps is redundant given the fromLayer !== toLayer short-circuit on line 322 — harmless, just noise.
  • adapter-components intentionally cannot import from types/, matching the components layer. Worth double-checking that any shared Zod schemas used by adapter components are re-exported through store/utils rather than imported from types/ directly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__architecture__/validate.ts` around lines 71 - 86, Remove the redundant
self-reference in the 'adapter-components' rule's allowedDeps array in
validate.ts: drop the 'adapter-components' entry (it’s unnecessary because
getLayer() plus the fromLayer !== toLayer short-circuit prevents same-layer
checks) and run tests; additionally, verify any shared Zod schemas used by
middleware/adapters/<adapter>/components are re-exported through allowed layers
like store or utils (not imported directly from types) to preserve the intended
layering constraints.
src/frontend/store/slices/ai/types.ts (1)

69-69: Optional: constant name no longer matches the model.

After the move to a flat project-scoped list, MAX_CONVERSATION_MESSAGES reads as stale (there are no conversations anymore). Consider renaming to MAX_MESSAGES in a follow-up — keep it in lockstep with openplc-web to preserve the byte-identical contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/store/slices/ai/types.ts` at line 69, Rename the stale constant
MAX_CONVERSATION_MESSAGES to MAX_MESSAGES and update all references/usages
(imports, selectors, reducers, tests) to the new symbol (e.g., replace
MAX_CONVERSATION_MESSAGES with MAX_MESSAGES in the ai slice and any consumers);
ensure the exported name and value remain unchanged except for the identifier so
the runtime behavior is identical and keep this change in sync (byte-identical
contract) with openplc-web.
🤖 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/frontend/components/_features/`[workspace]/editor/monaco/index.tsx:
- Around line 946-961: The handlers handlePouUpdated, handleAcceptAllHunks, and
handleRejectAllHunks close over the prop name and can act on a stale POU when
the editor persists; update each handler to read the current POU name from
openPLCStoreBase.getState().editor.meta.name (same pattern as
handleInsertAtCursor) instead of using the closed-over name, so the check
against targetPou uses the runtime store value before applying edits via
editorInstance, toggling isSyncingModelRef.current, and calling setLocalText as
needed.

---

Nitpick comments:
In `@src/__architecture__/validate.ts`:
- Around line 67-70: The adapters layer label is narrower than the current
classifier: getLayer() uses rel.startsWith('middleware/adapters/') and therefore
matches any file under that directory (not just services/), so either (A)
broaden the adapters.name to accurately describe the catch-all scope or (B)
tighten getLayer() to only classify service files and top-level adapter modules
by changing the condition that uses rel.startsWith('middleware/adapters/') to
require either a '/services/' segment or a top-level adapter filename pattern
(e.g., match 'middleware/adapters/*.ts'); update the adapters.name or the
getLayer() matching logic accordingly so the reported layer and classifier stay
consistent.
- Around line 71-86: Remove the redundant self-reference in the
'adapter-components' rule's allowedDeps array in validate.ts: drop the
'adapter-components' entry (it’s unnecessary because getLayer() plus the
fromLayer !== toLayer short-circuit prevents same-layer checks) and run tests;
additionally, verify any shared Zod schemas used by
middleware/adapters/<adapter>/components are re-exported through allowed layers
like store or utils (not imported directly from types) to preserve the intended
layering constraints.

In `@src/frontend/store/slices/ai/types.ts`:
- Line 69: Rename the stale constant MAX_CONVERSATION_MESSAGES to MAX_MESSAGES
and update all references/usages (imports, selectors, reducers, tests) to the
new symbol (e.g., replace MAX_CONVERSATION_MESSAGES with MAX_MESSAGES in the ai
slice and any consumers); ensure the exported name and value remain unchanged
except for the identifier so the runtime behavior is identical and keep this
change in sync (byte-identical contract) with openplc-web.
🪄 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: 1d24350a-0a69-46ab-b643-ac9820bd63e9

📥 Commits

Reviewing files that changed from the base of the PR and between 28d322f and 70e4a43.

📒 Files selected for processing (7)
  • src/__architecture__/validate.ts
  • src/backend/shared/styles/globals.css
  • src/frontend/components/_features/[workspace]/editor/monaco/index.tsx
  • src/frontend/store/__tests__/ai-slice.test.ts
  • src/frontend/store/slices/ai/index.ts
  • src/frontend/store/slices/ai/slice.ts
  • src/frontend/store/slices/ai/types.ts

Comment on lines +946 to +961
// Listen for AI tool updates — sync editor model with new body
const handlePouUpdated = (e: Event) => {
const { pouName: targetPou, body } = (e as CustomEvent<{ pouName: string; body: string; oldBody?: string }>)
.detail
if (targetPou !== name) return
setDiffReview({ active: true, proposedBody, variableSummary })
}
window.addEventListener('ai-review-code', handleCodeReview)

// Listen for AI chat "apply code" — applies after the user accepted the diff
const handleCodeApplied = (e: Event) => {
const { pouName: targetPou, body } = (e as CustomEvent<{ pouName: string; body: string }>).detail
if (targetPou !== name) return
const model = editorInstance.getModel()
if (model) {
if (model && model.getValue() !== body) {
isSyncingModelRef.current = true
const fullRange = model.getFullModelRange()
editorInstance.executeEdits('ai-code-apply', [{ range: fullRange, text: body }])
editorInstance.executeEdits('ai-tool-update', [{ range: fullRange, text: body }])
isSyncingModelRef.current = false
editorInstance.focus()
}
setLocalText(body)
}
window.addEventListener('ai-pou-updated', handlePouUpdated)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether the parent remounts MonacoEditor per POU (key strategy) or reuses it.
rg -nP -C4 '<MonacoEditor\b' --type=tsx --type=ts
# Also check how tab/editor switching is handled around this component.
rg -nP -C3 'keepCurrentModel|onDidChangeModel' --type=ts --type=tsx

Repository: Autonomy-Logic/openplc-editor

Length of output: 128


🏁 Script executed:

#!/bin/bash
# Search for MonacoEditor component usage (corrected approach)
rg 'MonacoEditor' -A 5 -B 2 --max-count=10

Repository: Autonomy-Logic/openplc-editor

Length of output: 7182


🏁 Script executed:

#!/bin/bash
# Look at the exact file and lines mentioned in the review
head -1000 src/frontend/components/_features/\[workspace\]/editor/monaco/index.tsx | tail -150 | head -100

Repository: Autonomy-Logic/openplc-editor

Length of output: 3711


🏁 Script executed:

#!/bin/bash
# Find PrimitiveEditor usage to check key and keepCurrentModel props
rg 'PrimitiveEditor|keepCurrentModel|onDidChangeModel' -B 3 -A 3

Repository: Autonomy-Logic/openplc-editor

Length of output: 4607


🏁 Script executed:

#!/bin/bash
# Get full context around handlePouUpdated registration with cleanup
sed -n '900,1000p' src/frontend/components/_features/\[workspace\]/editor/monaco/index.tsx

Repository: Autonomy-Logic/openplc-editor

Length of output: 3872


🏁 Script executed:

#!/bin/bash
# Check MonacoEditor component instantiation in workspace-screen for key prop
rg -A 8 '<MonacoEditor' src/frontend/screens/workspace-screen.tsx

Repository: Autonomy-Logic/openplc-editor

Length of output: 544


🏁 Script executed:

#!/bin/bash
# Look for useEffect cleanup of the ai-pou-updated listener
rg -B 10 -A 5 "removeEventListener.*ai-pou-updated|ai-pou-updated.*removeEventListener" src/frontend/components/_features/\[workspace\]/editor/monaco/index.tsx

Repository: Autonomy-Logic/openplc-editor

Length of output: 744


Fix stale name closure in AI tool event handlers to use dynamic store lookup.

handlePouUpdated, handleAcceptAllHunks, and handleRejectAllHunks close over the name prop at mount time. Since the editor instance persists across tab switches when key={undefined} and keepCurrentModel={true} (Electron mode), these listeners remain active with a stale name value when the user switches POUs. The ai-pou-updated event will then either drop updates for the actual target POU or apply them to the wrong editor instance.

Use openPLCStoreBase.getState().editor.meta.name dynamically inside each handler, matching the pattern already established in handleInsertAtCursor (line 924).

🛠 Suggested shape (resolve current POU dynamically)
-    const handlePouUpdated = (e: Event) => {
-      const { pouName: targetPou, body } = (e as CustomEvent<{ pouName: string; body: string; oldBody?: string }>)
-        .detail
-      if (targetPou !== name) return
+    const handlePouUpdated = (e: Event) => {
+      const { pouName: targetPou, body } = (e as CustomEvent<{ pouName: string; body: string; oldBody?: string }>)
+        .detail
+      const currentName = openPLCStoreBase.getState().editor.meta.name
+      if (targetPou !== currentName) return

Apply the same treatment to handleAcceptAllHunks and handleRejectAllHunks.

📝 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.

Suggested change
// Listen for AI tool updates — sync editor model with new body
const handlePouUpdated = (e: Event) => {
const { pouName: targetPou, body } = (e as CustomEvent<{ pouName: string; body: string; oldBody?: string }>)
.detail
if (targetPou !== name) return
setDiffReview({ active: true, proposedBody, variableSummary })
}
window.addEventListener('ai-review-code', handleCodeReview)
// Listen for AI chat "apply code" — applies after the user accepted the diff
const handleCodeApplied = (e: Event) => {
const { pouName: targetPou, body } = (e as CustomEvent<{ pouName: string; body: string }>).detail
if (targetPou !== name) return
const model = editorInstance.getModel()
if (model) {
if (model && model.getValue() !== body) {
isSyncingModelRef.current = true
const fullRange = model.getFullModelRange()
editorInstance.executeEdits('ai-code-apply', [{ range: fullRange, text: body }])
editorInstance.executeEdits('ai-tool-update', [{ range: fullRange, text: body }])
isSyncingModelRef.current = false
editorInstance.focus()
}
setLocalText(body)
}
window.addEventListener('ai-pou-updated', handlePouUpdated)
// Listen for AI tool updates — sync editor model with new body
const handlePouUpdated = (e: Event) => {
const { pouName: targetPou, body } = (e as CustomEvent<{ pouName: string; body: string; oldBody?: string }>)
.detail
const currentName = openPLCStoreBase.getState().editor.meta.name
if (targetPou !== currentName) return
const model = editorInstance.getModel()
if (model && model.getValue() !== body) {
isSyncingModelRef.current = true
const fullRange = model.getFullModelRange()
editorInstance.executeEdits('ai-tool-update', [{ range: fullRange, text: body }])
isSyncingModelRef.current = false
}
setLocalText(body)
}
window.addEventListener('ai-pou-updated', handlePouUpdated)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/components/_features/`[workspace]/editor/monaco/index.tsx around
lines 946 - 961, The handlers handlePouUpdated, handleAcceptAllHunks, and
handleRejectAllHunks close over the prop name and can act on a stale POU when
the editor persists; update each handler to read the current POU name from
openPLCStoreBase.getState().editor.meta.name (same pattern as
handleInsertAtCursor) instead of using the closed-over name, so the check
against targetPou uses the runtime store value before applying edits via
editorInstance, toggling isSyncingModelRef.current, and calling setLocalText as
needed.

Security update — fixes mXSS, prototype pollution, and config bypass
vulnerabilities. Addresses openplc-web Dependabot PR #359.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dcoutinho1328 dcoutinho1328 force-pushed the refactor/ai-chat-pr353 branch from e7dd827 to 672e71b Compare April 16, 2026 23:45
dcoutinho1328 and others added 2 commits April 21, 2026 22:12
- Remove AIStatusIndicator badge from Monaco editor (removed in PR #353 per Thiago's request)
- Restore per-hunk inline diff review with Keep/Undo buttons per change
- Add ai-diff-review utility (computeHunks, applyAcceptedHunks)
- Add Monaco ai-diff-review renderer (green/red decorations + floating buttons)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/frontend/components/_features/[workspace]/editor/monaco/ai-diff-review.ts (1)

77-142: Consider consolidating the per-hunk scroll listener and replacing the any-cast disposable storage.

Two minor refinements in this block:

  1. One editor.onDidScrollChange listener is registered per hunk (line 134) and each listener only updates one container. For a large refactor touching many hunks this means N listeners fire on every scroll. A single listener that iterates all containers would be cheaper and easier to dispose.
  2. Stashing the disposable on the DOM node via (container as any)._scrollDisposable requires eslint-disables (L18-19, L140-141, L154-155). A module-scoped WeakMap<HTMLElement, monaco.IDisposable> keeps the code typesafe and avoids the any escape hatch.
♻️ Sketch
+const scrollDisposables = new WeakMap<HTMLElement, monaco.IDisposable>()
 ...
-  // Update position on scroll
-  const scrollDisposable = editor.onDidScrollChange(() => {
-    const newTop = Math.max(0, editor.getTopForLineNumber(hunk.startLine) - editor.getScrollTop())
-    container.style.top = `${newTop}px`
-  })
-
-  // Store disposable for cleanup
-  // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-  ;(container as any)._scrollDisposable = scrollDisposable
+  // Position updates handled by the shared scroll listener below.

Then register a single listener outside the per-hunk loop that walks buttonContainers and repositions each. Cleanup disposes that one listener plus any entries in the WeakMap for stale containers.

As per coding guidelines, .ts/.tsx files should avoid any types; the WeakMap variant removes all three any-disables in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/monaco/ai-diff-review.ts
around lines 77 - 142, The per-hunk scroll listener registration inside the loop
(editor.onDidScrollChange) causes N listeners and the code stashes disposables
on DOM nodes via (container as any)._scrollDisposable; replace this by creating
a module-scoped WeakMap<HTMLElement, monaco.IDisposable> to store disposables
and register a single editor.onDidScrollChange listener outside the hunks loop
that iterates buttonContainers and recalculates each container.style.top using
editor.getTopForLineNumber(hunk.startLine) - editor.getScrollTop(); remove the
per-hunk listener creation and the (container as any) cast/eslint-disable
usages, ensure the single listener is disposed during cleanup and entries are
removed from the WeakMap when containers are removed.
src/frontend/components/_features/[workspace]/editor/monaco/index.tsx (1)

181-286: Refactor: acceptedHunks naming is inverted, and handleUndoHunk has a dead if/else plus an unused value.

Small cleanups that would make this state machine much easier to follow:

  1. acceptedHunks is initialized on L1072 with every hunk id and IDs are deleted from it as hunks get resolved (Keep or Undo). It is used on L216 to derive pending hunks. The field semantically represents "still-pending" hunks; rename to pendingHunks (or unresolvedHunks) to avoid confusion with actually-accepted-and-kept hunks.
  2. In handleUndoHunk (L243-253) both branches of if (!newAccepted.has(h.id)) … else … add the hunk to keptIds, so the branching is dead — this can collapse to a single statement (or be replaced with keptIds = new Set(prev.hunks.map(h => h.id).filter(id => id !== hunkId))).
  3. remainingHunks is computed on L272 and then never read; freshHunks from computeHunks fully replaces it on L276-280. Drop L272-273 or use remainingHunks.length === 0 as the sole short-circuit and skip the recompute.
♻️ Illustrative diff for (2) and (3)
-        // Rebuild body: accepted hunks keep new code, this rejected hunk keeps old code
-        const keptIds = new Set<string>()
-        for (const h of prev.hunks) {
-          if (h.id === hunkId) continue // This one is undone
-          if (!newAccepted.has(h.id)) {
-            // Already resolved as kept
-            keptIds.add(h.id)
-          } else {
-            // Still pending — treat as kept for now (new code)
-            keptIds.add(h.id)
-          }
-        }
+        // Rebuild body: every hunk except the one being undone keeps its new code.
+        const keptIds = new Set(prev.hunks.map((h) => h.id).filter((id) => id !== hunkId))
         ...
-        // Recompute hunks for remaining pending changes
-        const remainingHunks = prev.hunks.filter((h) => newAccepted.has(h.id))
-        if (remainingHunks.length === 0) return null
-
-        // Recompute line positions for remaining hunks
         const freshHunks = computeHunks(prev.oldBody, newBody)
-        const freshAccepted = new Set(freshHunks.map((h) => h.id))
-
         if (freshHunks.length === 0) return null
-        return { ...prev, newBody, hunks: freshHunks, acceptedHunks: freshAccepted }
+        return { ...prev, newBody, hunks: freshHunks, acceptedHunks: new Set(freshHunks.map((h) => h.id)) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/components/_features/`[workspace]/editor/monaco/index.tsx around
lines 181 - 286, The state field acceptedHunks is semantically inverted (it
actually tracks unresolved/pending hunks) and should be renamed to pendingHunks
(update its initializer and every usage in setDiffReview, the pendingHunks
derivation, renderDiffReview call, and any code that mutates it) to avoid
confusion; inside handleUndoHunk simplify the keptIds construction by removing
the dead if/else and directly building keptIds as all hunk ids except the undone
hunk (or use a map/filter on prev.hunks), remove the unused remainingHunks
variable before recomputing freshHunks (drop the L272-273 style check or replace
it with a single short-circuit using freshHunks.length), and ensure subsequent
references to acceptedHunks are updated to pendingHunks (affecting computeHunks,
freshAccepted, applyAcceptedHunks, and the setDiffReview return).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Line 110: The package.json currently lists the redundant devDependency
"@types/diff": "^7.0.2" while the installed "diff@^9.0.0" already includes its
own TypeScript definitions; remove the "@types/diff" entry from package.json
(devDependencies) so TypeScript uses the bundled types, then reinstall/update
your lockfile (npm install / yarn install) to ensure the lockfile reflects the
removal and run a quick TypeScript build to verify no type regressions.

In `@src/frontend/components/_features/`[workspace]/editor/monaco/index.tsx:
- Around line 1044-1055: When oldBody === undefined the code updates the Monaco
model and localText but never persists to the store; change the branch so it
first reads the current editor name once from
openPLCStoreBase.getState().editor.meta.name to avoid stale closure, then
perform the model edit with isSyncingModelRef.current toggled as before,
setLocalText(body), and finally call updatePou(name, { body }) to persist the
change; keep isSyncingModelRef usage so onChange/handleWriteInPou still
short-circuits during the programmatic edit but ensure updatePou is invoked
explicitly after unsetting the flag.

---

Nitpick comments:
In
`@src/frontend/components/_features/`[workspace]/editor/monaco/ai-diff-review.ts:
- Around line 77-142: The per-hunk scroll listener registration inside the loop
(editor.onDidScrollChange) causes N listeners and the code stashes disposables
on DOM nodes via (container as any)._scrollDisposable; replace this by creating
a module-scoped WeakMap<HTMLElement, monaco.IDisposable> to store disposables
and register a single editor.onDidScrollChange listener outside the hunks loop
that iterates buttonContainers and recalculates each container.style.top using
editor.getTopForLineNumber(hunk.startLine) - editor.getScrollTop(); remove the
per-hunk listener creation and the (container as any) cast/eslint-disable
usages, ensure the single listener is disposed during cleanup and entries are
removed from the WeakMap when containers are removed.

In `@src/frontend/components/_features/`[workspace]/editor/monaco/index.tsx:
- Around line 181-286: The state field acceptedHunks is semantically inverted
(it actually tracks unresolved/pending hunks) and should be renamed to
pendingHunks (update its initializer and every usage in setDiffReview, the
pendingHunks derivation, renderDiffReview call, and any code that mutates it) to
avoid confusion; inside handleUndoHunk simplify the keptIds construction by
removing the dead if/else and directly building keptIds as all hunk ids except
the undone hunk (or use a map/filter on prev.hunks), remove the unused
remainingHunks variable before recomputing freshHunks (drop the L272-273 style
check or replace it with a single short-circuit using freshHunks.length), and
ensure subsequent references to acceptedHunks are updated to pendingHunks
(affecting computeHunks, freshAccepted, applyAcceptedHunks, and the
setDiffReview return).
🪄 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: 3fb4dc89-8491-4dc0-bf56-12c77db8c533

📥 Commits

Reviewing files that changed from the base of the PR and between 70e4a43 and 1efb52c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • package.json
  • src/frontend/components/_features/[workspace]/editor/monaco/ai-diff-review.ts
  • src/frontend/components/_features/[workspace]/editor/monaco/ai-status-indicator.tsx
  • src/frontend/components/_features/[workspace]/editor/monaco/index.tsx
  • src/frontend/utils/ai-diff-review.ts
💤 Files with no reviewable changes (1)
  • src/frontend/components/_features/[workspace]/editor/monaco/ai-status-indicator.tsx

Comment thread package.json
"@teamsupercell/typings-for-css-modules-loader": "^2.5.2",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^14.0.0",
"@types/diff": "^7.0.2",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm no code relies on resolution from `@types/diff` specifically and that
# imports from 'diff' resolve against the bundled types in v9.
rg -nP --type=ts --type=tsx -C2 "from\s+['\"]diff['\"]" 
rg -nP --type=json -C1 '"@types/diff"'

Repository: Autonomy-Logic/openplc-editor

Length of output: 448


🏁 Script executed:

#!/bin/bash
# Check if diff@^9.0.0 is at line 64 in dependencies
sed -n '60,70p' package.json

# Search for imports of 'diff' in all source files (not just TS/TSX)
rg -n "from\s+['\"]diff['\"]|require\s*\(\s*['\"]diff['\"]" --type-list | head -20
rg -rn "from ['\"]diff['\"]|require(['\"]diff['\"])"

# Check the structure of devDependencies around line 110
sed -n '105,115p' package.json

Repository: Autonomy-Logic/openplc-editor

Length of output: 1297


🏁 Script executed:

#!/bin/bash
# Get the full import statement from the ai-diff-review.ts file
cat -n src/frontend/utils/ai-diff-review.ts | head -50

# Also search more carefully for all uses of 'diff'
rg -n "import.*diff|from.*diff|require.*diff" src/

Repository: Autonomy-Logic/openplc-editor

Length of output: 3267


🌐 Web query:

diff npm package v9 type definitions shipped

💡 Result:

No, the "diff" npm package (kpdecker/jsdiff) version 9 type definitions have not shipped as of 2026-04-22. The latest released version is 8.0.4 (published around 2026-03-23). Type definitions were first shipped in v8.0.0 (May 2025), making @types/diff obsolete. Version 9.0.0 exists as a prerelease on GitHub (drops ES5 support, uses TextDecoder/Uint8Array, compiled to ES6 target), but it is not published to npm. GitHub tags confirm no v9 release; latest is 8.0.4. npm/cli recently updated to diff@8.0.3 (Jan 2026).

Citations:


🏁 Script executed:

#!/bin/bash
# Check the actual resolved version in package-lock.json
grep -A 5 '"diff"' package-lock.json | head -20

# Also check if there's a resolved field with a URL
rg -n '"diff"' package-lock.json -A 3

Repository: Autonomy-Logic/openplc-editor

Length of output: 710


🏁 Script executed:

#!/bin/bash
# Find the node_modules/diff entry in package-lock.json
jq '.packages["node_modules/diff"]' package-lock.json 2>/dev/null || {
  # Fallback if jq doesn't work
  sed -n '/node_modules\/diff/,/^      }/p' package-lock.json | head -20
}

Repository: Autonomy-Logic/openplc-editor

Length of output: 348


Remove redundant @types/diffdiff v9 ships its own type definitions.

The @types/diff package has been deprecated; its author explicitly states it is a stub types definition and that "diff provides its own type definitions, so you do not need this installed." From the diff package changelog: type definitions were first shipped in version 8.0.0 (May 2025).

Beyond being redundant, the major versions are mismatched (@types/diff@^7.0.2 vs diff@^9.0.0), which can cause TypeScript to pick up outdated stub types on some resolution setups. Drop the dev dependency and rely on the types bundled with diff.

📦 Suggested fix
-    "@types/diff": "^7.0.2",
     "@types/eslint": "^9.6.1",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 110, The package.json currently lists the redundant
devDependency "@types/diff": "^7.0.2" while the installed "diff@^9.0.0" already
includes its own TypeScript definitions; remove the "@types/diff" entry from
package.json (devDependencies) so TypeScript uses the bundled types, then
reinstall/update your lockfile (npm install / yarn install) to ensure the
lockfile reflects the removal and run a quick TypeScript build to verify no type
regressions.

Comment on lines +1044 to +1055
// If no old body provided (backward compat), apply directly
if (oldBody === undefined) {
const model = editorInstance.getModel()
if (model && model.getValue() !== body) {
isSyncingModelRef.current = true
const fullRange = model.getFullModelRange()
editorInstance.executeEdits('ai-tool-update', [{ range: fullRange, text: body }])
isSyncingModelRef.current = false
}
setLocalText(body)
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Backward-compat branch updates editor but not the store — changes will be lost on remount/tab switch.

When oldBody === undefined, this branch calls editor.executeEdits(...) with isSyncingModelRef.current = true and then setLocalText(body). Because the sync flag is set, the onChangehandleWriteInPou path short-circuits at L1240 (if (isSyncingModelRef.current) return) and updatePou is never called. The diff-review branch below (L269 / L376) does the opposite and calls updatePou explicitly.

Net effect: the Monaco model and local localText show the AI's new body, but pou.body.value in the store still holds the old body. As soon as the POU effect at L195-207 re-syncs localText from openPLCStoreBase.getState().project.data.pous (e.g. on tab switch, or when pous updates for any unrelated reason), the AI's update silently reverts.

🐛 Suggested fix
       // If no old body provided (backward compat), apply directly
       if (oldBody === undefined) {
         const model = editorInstance.getModel()
         if (model && model.getValue() !== body) {
           isSyncingModelRef.current = true
           const fullRange = model.getFullModelRange()
           editorInstance.executeEdits('ai-tool-update', [{ range: fullRange, text: body }])
           isSyncingModelRef.current = false
         }
         setLocalText(body)
+        // Persist to the store so the change survives remounts / tab switches.
+        openPLCStoreBase.getState().projectActions.updatePou({
+          name,
+          content: { language, value: body },
+        })
         return
       }

Note: the same name used for the store write here is subject to the stale-closure problem already flagged in the previous review — resolve both together by reading openPLCStoreBase.getState().editor.meta.name once at the top of the handler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/components/_features/`[workspace]/editor/monaco/index.tsx around
lines 1044 - 1055, When oldBody === undefined the code updates the Monaco model
and localText but never persists to the store; change the branch so it first
reads the current editor name once from
openPLCStoreBase.getState().editor.meta.name to avoid stale closure, then
perform the model edit with isSyncingModelRef.current toggled as before,
setLocalText(body), and finally call updatePou(name, { body }) to persist the
change; keep isSyncingModelRef usage so onChange/handleWriteInPou still
short-circuits during the programmatic edit but ensure updatePou is invoked
explicitly after unsetting the flag.

@dcoutinho1328 dcoutinho1328 merged commit 70fda52 into development Apr 22, 2026
20 of 21 checks passed
@dcoutinho1328 dcoutinho1328 deleted the refactor/ai-chat-pr353 branch April 22, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant