Skip to content

feat(ai): port shared chat-history persistence + UX fixes from openplc-web#747

Merged
JoaoGSP merged 4 commits into
developmentfrom
feat/ai-chat-history-persistence-shared
May 7, 2026
Merged

feat(ai): port shared chat-history persistence + UX fixes from openplc-web#747
JoaoGSP merged 4 commits into
developmentfrom
feat/ai-chat-history-persistence-shared

Conversation

@JoaoGSP
Copy link
Copy Markdown
Member

@JoaoGSP JoaoGSP commented May 5, 2026

Summary

Mirrors the shared-layer changes from openplc-web#385 so the editor and web don't drift on what the AI feature exposes through the shared port and store layers.

Web adapter–specific code is intentionally NOT included — the editor's AI integration is a future workstream. This PR only ports the bits both repos share.

What's mirrored

File Change
middleware/shared/ports/types.ts New AIChatContentBlock type; ChatMessage.content widens to string | AIChatContentBlock[]; new optional ChatMessage.conversationId
frontend/store/slices/ai/types.ts + slice.ts conversationId, conversations, isLoadingConversation state; 7 new actions (setConversationId, setConversations, prependConversation, removeConversation, updateConversationTitle, replaceMessages, setLoadingConversation); clearConversation also nulls conversationId; updateMessageContent accepts block-array content
frontend/store/__tests__/ai-slice.test.ts 17 new tests (51 → 68, 100% slice coverage maintained)
frontend/store/slices/shared/slice.ts clearStatesOnCloseProject now calls aiActions.clearConversation() so chat doesn't bleed across project switches
frontend/store/slices/shared/types.ts SharedRootState gains AISlice & so the shared slice's typed getState() can call aiActions.*
frontend/components/_features/[app]/toast/index.tsx Viewport moves to top-4 left-1/2 -translate-x-1/2; slide-in consistently from-top
frontend/screens/workspace-screen.tsx Chat panel ResizablePanel defaultSize 16% → 30%, range 16-25 → 20-50

What's NOT included (web-specific)

  • middleware/adapters/web/services/ai/conversations/* — TanStack Query hooks bound to /ai/conversations
  • middleware/adapters/web/components/ai-chat/* — chat panel, switcher, message bubble, agentic loop
  • router/pages/index-page.tsx — web routing
  • The mid-stream lastLoadedConversationRef fix — only matters once a chat panel exists in the editor

Notable type fix

SharedRootState was missing the AISlice intersection in both repos. The editor's stricter tsc invocation surfaced it; web's happens to skip the check. Adding AISlice is the correct shape since the AI slice IS shared, so this PR fixes it here. A follow-up will mirror the same fix on web.

Validation

  • pnpm run lint — 0 errors, 2 pre-existing warnings
  • pnpm run validate:arch — passes
  • pnpm exec tsc --noEmit — clean
  • jest src/frontend/store/__tests__/ai-slice.test.ts68/68 pass

Tracking

Test plan

  • CI passes
  • Smoke: open the editor, switch projects — no console errors related to the new state shape
  • Toast appears at top-center (try a Save action)
  • Workspace layout looks unchanged when AI chat isn't enabled (it's not on editor — chat panel is gated by capabilities.hasAIAssistant)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added conversation management: create, save, switch, prepend, remove, and rename AI conversations; loading state for conversations.
    • Message handling now supports rich content blocks in addition to plain text.
  • UI/UX Changes

    • Toast notifications moved to top-center with horizontal swipe/slide animations.
    • Chat sidebar resizing constraints expanded for greater flexibility.
    • Active chat automatically clears when switching projects.

…c-web

Mirrors the shared-layer changes from
[openplc-web#385](Autonomy-Logic/openplc-web#385).
Editor and web share `middleware/shared/ports/`, the AI slice, the
shared workspace slice, the toast viewport, and the workspace screen
layout — keeping these in sync prevents the two adapters from
drifting on what the AI feature exposes to the platform.

What's mirrored from web's frontend branch:

  - middleware/shared/ports/types.ts:
      - new `AIChatContentBlock` type (text / tool_use / tool_result)
      - `ChatMessage.content` widens to `string | AIChatContentBlock[]`
      - new optional `ChatMessage.conversationId`
  - frontend/store/slices/ai/types.ts + slice.ts:
      - `conversationId`, `conversations: ConversationSummary[]`,
        `isLoadingConversation` state
      - 7 new actions: setConversationId, setConversations,
        prependConversation, removeConversation,
        updateConversationTitle, replaceMessages, setLoadingConversation
      - `clearConversation` now also nulls `conversationId`
      - `updateMessageContent` widens to accept block-array content
  - frontend/store/__tests__/ai-slice.test.ts: 17 new tests covering
    all of the above. 68/68 passing.
  - frontend/store/slices/shared/slice.ts: `clearStatesOnCloseProject`
    now calls `aiActions.clearConversation()` so chat doesn't bleed
    across project switches.
  - frontend/store/slices/shared/types.ts: SharedRootState gains
    `AISlice &` so the typed `getState()` inside the shared slice
    can call `aiActions.*`. The web copy lacks this intersection but
    its tsc happens to skip the check; adding it everywhere is the
    correct shape since the AI slice is shared. (Web fix tracked as
    a follow-up.)
  - frontend/components/_features/[app]/toast/index.tsx: viewport
    moves to `top-4 left-1/2 -translate-x-1/2`. Slide-in animation
    consistently from-top.
  - frontend/screens/workspace-screen.tsx: chat panel ResizablePanel
    `defaultSize` 16% → 30%, range 16-25 → 20-50.

What's NOT mirrored (web-adapter-specific):
  - middleware/adapters/web/services/ai/conversations/* (TanStack
    Query hooks bound to /ai/conversations) — the editor's AI
    integration is a future workstream; conversation persistence
    will land in the editor when its AI adapter is wired up.
  - middleware/adapters/web/components/ai-chat/* (chat panel,
    switcher, message bubble, agentic loop) — same reason.
  - router/pages/index-page.tsx (web routing).
  - The agentic-loop / panel persistence wiring fix
    (`lastLoadedConversationRef`) — only relevant once an AI chat
    panel exists in the editor.

Validation:
  - pnpm run lint                — 0 errors, 2 pre-existing warnings
  - pnpm run validate:arch       — passes
  - pnpm exec tsc --noEmit       — clean
  - jest src/frontend/store/__tests__/ai-slice.test.ts — 68/68 pass

Tracking: epic DOPE-270 (shared layer changes mirrored).

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

coderabbitai Bot commented May 5, 2026

Walkthrough

Conversation tracking and message block support were added to the AI Redux slice (state, types, actions, tests). The shared root state now includes the AI slice and clears conversations on project close. Separately, UI tweaks adjust toast positioning/animation and widen the workspace chat panel sizing.

Changes

Conversation Management & State Integration

Layer / File(s) Summary
Type additions / wire format
src/middleware/shared/ports/types.ts, src/frontend/store/slices/ai/types.ts
Introduce AIChatContentBlock; ChatMessage.content becomes `string
State shape
src/frontend/store/slices/ai/types.ts, src/frontend/store/slices/ai/slice.ts
AIState.ai and DEFAULT_AI_STATE gain `conversationId: string
Slice actions / implementation
src/frontend/store/slices/ai/slice.ts
Add setConversationId, setConversations, prependConversation (dedupe), removeConversation (clears active/messages if removed), updateConversationTitle, replaceMessages (caps to MAX_CONVERSATION_MESSAGES, clears error), setLoadingConversation; clearConversation now also nulls conversationId.
Shared-state integration
src/frontend/store/slices/shared/types.ts, src/frontend/store/slices/shared/slice.ts
SharedRootState now includes AISlice &; project-close flow calls aiActions.clearConversation() to drop active conversation and messages.
Tests
src/frontend/store/__tests__/ai-slice.test.ts
Extend initial-state assertions for conversation fields; update clearConversation tests; add comprehensive conversation-management test suite (set/clear id, replace/prepend/remove/update conversations, replaceMessages behavior, loading flag) and test for block-array message content updates.

UI Positioning & Layout Tweaks

Layer / File(s) Summary
Toast positioning & animation
src/frontend/components/_features/[app]/toast/index.tsx
ToastViewport moved from bottom-right to top-centered (left-1/2, top-4, -translate-x-1/2); toast swipe/slide classes changed from vertical (translate-y-*, bottom full) to horizontal (translate-x-*, right full).
Workspace chat sizing
src/frontend/screens/workspace-screen.tsx
ResizablePanel sizing props changed: defaultSize 16→30, minSize 16→20, maxSize 25→50.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

feature

Suggested reviewers

  • thiagoralves
  • vmleroy
  • DanielBorgesDev

Poem

🐇 I hopped through state and types with care,
Conversations sprouted everywhere.
Messages in blocks and toasts on high,
Chat pane widened—wave hello to the sky!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: porting shared chat-history persistence features and UX fixes from openplc-web, which aligns with the PR's core objectives.
Description check ✅ Passed The PR description is comprehensive with clear sections covering summary, mirrored changes, exclusions, validation results, and tracking links, though it deviates from the template structure (lacks formal issue references and DOD checklist).
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ai-chat-history-persistence-shared

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/frontend/screens/workspace-screen.tsx (1)

661-670: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

defaultSize overflow: all three panels sum to 114% when chat is open.

Before this PR, 16 (explorer) + 68 (workspace) + 16 (chat) = 100% exactly. After the bump to defaultSize={30} the sum becomes 114%, so react-resizable-panels will silently normalize on mount. The chat panel will actually open at ~26% (≈ 30/114 × 100), not the intended 30%, and the workspace panel will be squeezed to ~60% instead of 68%.

A secondary constraint conflict also exists: chatPanel.maxSize (50) + workspacePanel.minSize (50) = 100%, which leaves exactly 0% for the explorer/source-control panel whenever the user drags chat to its maximum — collapsing the explorer entirely.

🛠️ Proposed fix — keep the three-panel total at 100% when chat is open

Lower the workspace panel's defaultSize by the chat panel's new default contribution (30 − 16 = 14 pp), and tighten the chat maxSize to leave at least the explorer's minimum room:

 <ResizablePanel
   id='workspacePanel'
   order={2}
-  defaultSize={68}
+  defaultSize={54}
   minSize={50}
   className='flex h-full min-h-0 overflow-hidden'
 >
 <ResizablePanel
   id='chatPanel'
   order={3}
   defaultSize={30}
   minSize={20}
-  maxSize={50}
+  maxSize={40}
   className='relative flex h-full min-h-0 w-full'
 >

Alternatively, make workspacePanel.defaultSize reactive to isChatOpen:

-  defaultSize={68}
+  defaultSize={isChatOpen ? 54 : 68}

54 + 16 (explorer) + 30 (chat) = 100
workspacePanel.minSize(50) + chatPanel.maxSize(40) = 90 → leaves ≥ 10% for the explorer ✓

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/screens/workspace-screen.tsx` around lines 661 - 670, The three
ResizablePanel defaults now sum >100% causing silent normalization; update the
panel sizing so totals remain 100% and avoid zeroing the explorer: reduce the
workspace panel's defaultSize from 68 to 54 (so 54 + 16 explorer + 30 chat =
100) and tighten chatPanel.maxSize from 50 to 40 to guarantee slack for the
explorer (workspacePanel.minSize(50) + chatPanel.maxSize(40) = 90 leaves ≥10%
for explorer); alternatively make workspacePanel.defaultSize compute based on
isChatOpen (e.g., use 54 when isChatOpen true, otherwise keep previous default)
and ensure IDs 'chatPanel', 'workspacePanel', and 'explorerPanel' are adjusted
accordingly.
src/frontend/store/slices/ai/slice.ts (1)

143-154: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

isLoadingConversation not reset by clearConversation — stale loading indicator on project switch.

If the user closes a project while a conversation is being fetched (isLoadingConversation = true), clearConversation leaves that flag set. The next project would inherit a stale loading state in the chat panel until the flag is explicitly cleared by the next conversation operation.

🛡️ Proposed fix
 clearConversation: () => {
   setState(
     produce(({ ai }: AISlice) => {
       ai.messages = []
       ai.error = null
       // Drop the active conversation pointer so the next /ai/chat call
       // is treated as a fresh conversation. The list itself stays —
       // the user can still see prior conversations and resume one.
       ai.conversationId = null
+      ai.isLoadingConversation = false
     }),
   )
 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/store/slices/ai/slice.ts` around lines 143 - 154,
clearConversation currently resets ai.messages, ai.error and ai.conversationId
but leaves ai.isLoadingConversation untouched which can leave a stale loading
state; update the clearConversation handler (the function named
clearConversation that calls setState and produce on AISlice) to also set
ai.isLoadingConversation = false so any in-flight or leftover loading flag is
cleared when switching/closing projects.
🧹 Nitpick comments (2)
src/frontend/components/_features/[app]/toast/index.tsx (1)

20-20: 💤 Low value

Stale inline comment.

// -> For fail toasts to be on top referred to the previous bottom-right stacking intent. With the viewport now top-center and styling unrelated to fail-toast ordering on this element, the note is misleading mid-JSX. Consider removing it or rewording to describe the current z-[100] rationale.

✏️ Proposed cleanup
   <PrimitiveToast.ToastViewport
     ref={ref}
     className={cn(
       'absolute left-1/2 top-4 z-[100] flex h-fit max-h-36 w-[292px] -translate-x-1/2 px-4 xl:w-[420px]',
       className,
     )}
-    //  -> For fail toasts to be on top
     {...props}
   />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/components/_features/`[app]/toast/index.tsx at line 20, The
inline comment "// -> For fail toasts to be on top" in the JSX of the Toast
component is stale and misleading given the viewport is now top-center and
ordering is unrelated; update the comment by either removing it or replacing it
with a brief note explaining why the element has "z-[100]" (e.g., "ensure toasts
render above other UI elements") so future readers understand the z-index
intent; locate the comment in the Toast component JSX (index.tsx) and apply the
change near the element that includes the z-[100] class.
src/frontend/store/slices/ai/types.ts (1)

10-16: 💤 Low value

lastModel literal union may need widening if additional models are introduced.

'haiku' | 'sonnet' | null will cause a TypeScript error if the backend starts returning a third model label (e.g. 'opus'). Consider widening to string | null or documenting this as an intentional exhaustive union with a note to update alongside the backend.

♻️ Proposed change (if widening is preferred)
 export type ConversationSummary = {
   id: string
   title: string
-  lastModel: 'haiku' | 'sonnet' | null
+  /** Model label as normalized by the backend (e.g. 'haiku', 'sonnet'). */
+  lastModel: string | null
   createdAt: string
   updatedAt: string
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/store/slices/ai/types.ts` around lines 10 - 16, The
ConversationSummary type's lastModel field currently uses a narrow union
('haiku' | 'sonnet' | null) which will break if the backend returns new model
labels; update the lastModel type in the ConversationSummary declaration to
string | null (or another broader union) so it accepts any model label, and add
a short comment on the ConversationSummary type to remind maintainers to tighten
the type if the API becomes strictly enumerated in the future.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/frontend/screens/workspace-screen.tsx`:
- Around line 661-670: The three ResizablePanel defaults now sum >100% causing
silent normalization; update the panel sizing so totals remain 100% and avoid
zeroing the explorer: reduce the workspace panel's defaultSize from 68 to 54 (so
54 + 16 explorer + 30 chat = 100) and tighten chatPanel.maxSize from 50 to 40 to
guarantee slack for the explorer (workspacePanel.minSize(50) +
chatPanel.maxSize(40) = 90 leaves ≥10% for explorer); alternatively make
workspacePanel.defaultSize compute based on isChatOpen (e.g., use 54 when
isChatOpen true, otherwise keep previous default) and ensure IDs 'chatPanel',
'workspacePanel', and 'explorerPanel' are adjusted accordingly.

In `@src/frontend/store/slices/ai/slice.ts`:
- Around line 143-154: clearConversation currently resets ai.messages, ai.error
and ai.conversationId but leaves ai.isLoadingConversation untouched which can
leave a stale loading state; update the clearConversation handler (the function
named clearConversation that calls setState and produce on AISlice) to also set
ai.isLoadingConversation = false so any in-flight or leftover loading flag is
cleared when switching/closing projects.

---

Nitpick comments:
In `@src/frontend/components/_features/`[app]/toast/index.tsx:
- Line 20: The inline comment "// -> For fail toasts to be on top" in the JSX of
the Toast component is stale and misleading given the viewport is now top-center
and ordering is unrelated; update the comment by either removing it or replacing
it with a brief note explaining why the element has "z-[100]" (e.g., "ensure
toasts render above other UI elements") so future readers understand the z-index
intent; locate the comment in the Toast component JSX (index.tsx) and apply the
change near the element that includes the z-[100] class.

In `@src/frontend/store/slices/ai/types.ts`:
- Around line 10-16: The ConversationSummary type's lastModel field currently
uses a narrow union ('haiku' | 'sonnet' | null) which will break if the backend
returns new model labels; update the lastModel type in the ConversationSummary
declaration to string | null (or another broader union) so it accepts any model
label, and add a short comment on the ConversationSummary type to remind
maintainers to tighten the type if the API becomes strictly enumerated in the
future.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 18e215b4-abf2-43f0-a3c0-a2b33834aa12

📥 Commits

Reviewing files that changed from the base of the PR and between b6bc81d and 5c20ae0.

📒 Files selected for processing (8)
  • src/frontend/components/_features/[app]/toast/index.tsx
  • src/frontend/screens/workspace-screen.tsx
  • src/frontend/store/__tests__/ai-slice.test.ts
  • src/frontend/store/slices/ai/slice.ts
  • src/frontend/store/slices/ai/types.ts
  • src/frontend/store/slices/shared/slice.ts
  • src/frontend/store/slices/shared/types.ts
  • src/middleware/shared/ports/types.ts

CI's `format / Format Check` step (`prettier --check src/`) flagged
`src/frontend/store/slices/ai/slice.ts` after the slice extensions
landed. The patch I applied from openplc-web kept that repo's
formatting verbatim — but the editor uses a stricter Prettier config
that re-collapses some of the multi-line `produce()` callbacks.

Ran `pnpm exec prettier --write` on the file. Only that one file
needed reformatting; the full `prettier --check 'src/**/*.{ts,tsx}'`
now reports "All matched files use Prettier code style!".

Other CI checks already pass on this PR (lint, build, arch, tsc).

Co-Authored-By: Claude Opus 4.7 (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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/frontend/store/slices/ai/slice.ts (1)

143-152: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset isLoadingConversation in clearConversation to avoid stale loading state

clearConversation (Line 143-152) clears messages/error/conversation pointer but leaves isLoadingConversation unchanged. If this action is used during project close/reset, the UI can remain stuck in a loading state.

Proposed fix
       clearConversation: () => {
         setState(
           produce(({ ai }: AISlice) => {
             ai.messages = []
             ai.error = null
+            ai.isLoadingConversation = false
             // Drop the active conversation pointer so the next /ai/chat call
             // is treated as a fresh conversation. The list itself stays —
             // the user can still see prior conversations and resume one.
             ai.conversationId = null
           }),
         )
       },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/store/slices/ai/slice.ts` around lines 143 - 152, The
clearConversation reducer currently resets ai.messages, ai.error, and
ai.conversationId but leaves ai.isLoadingConversation untouched, which can leave
the UI stuck; update the clearConversation handler (the function named
clearConversation inside the AISlice produce block used with setState/produce)
to also set ai.isLoadingConversation = false so the loading flag is cleared when
a conversation is reset. Ensure you modify the produce callback that mutates the
ai object to include this assignment alongside the existing resets.
🧹 Nitpick comments (1)
src/frontend/store/slices/ai/slice.ts (1)

217-221: ⚡ Quick win

Avoid storing caller-owned array references in Zustand state

setConversations (Line 217-221) and replaceMessages (Line 254-259) assign incoming arrays directly. If callers mutate those arrays later, store state can change outside setState. Clone on write to keep state updates controlled.

Proposed fix
       setConversations: (conversations) => {
         setState(
           produce(({ ai }: AISlice) => {
-            ai.conversations = conversations
+            ai.conversations = [...conversations]
           }),
         )
       },
@@
       replaceMessages: (messages) => {
         setState(
           produce(({ ai }: AISlice) => {
             ai.messages =
-              messages.length > MAX_CONVERSATION_MESSAGES ? messages.slice(-MAX_CONVERSATION_MESSAGES) : messages
+              messages.length > MAX_CONVERSATION_MESSAGES
+                ? messages.slice(-MAX_CONVERSATION_MESSAGES)
+                : [...messages]
             ai.error = null
           }),
         )
       },

As per coding guidelines, "Never mutate state directly in Zustand slices; always use produce() from Immer for immutable updates".

Also applies to: 254-259

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/store/slices/ai/slice.ts` around lines 217 - 221,
setConversations and replaceMessages are assigning caller-owned arrays directly
into Zustand state; change these to clone the incoming arrays before assigning
(e.g., create a shallow copy of conversations and of message arrays) inside the
produce callback so the store never holds a reference to a mutable external
array—update the assignments in setConversations and replaceMessages to use
cloned copies (e.g., via slice/spread or a shallow map) when setting
ai.conversations and the messages array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/frontend/store/slices/ai/slice.ts`:
- Around line 143-152: The clearConversation reducer currently resets
ai.messages, ai.error, and ai.conversationId but leaves ai.isLoadingConversation
untouched, which can leave the UI stuck; update the clearConversation handler
(the function named clearConversation inside the AISlice produce block used with
setState/produce) to also set ai.isLoadingConversation = false so the loading
flag is cleared when a conversation is reset. Ensure you modify the produce
callback that mutates the ai object to include this assignment alongside the
existing resets.

---

Nitpick comments:
In `@src/frontend/store/slices/ai/slice.ts`:
- Around line 217-221: setConversations and replaceMessages are assigning
caller-owned arrays directly into Zustand state; change these to clone the
incoming arrays before assigning (e.g., create a shallow copy of conversations
and of message arrays) inside the produce callback so the store never holds a
reference to a mutable external array—update the assignments in setConversations
and replaceMessages to use cloned copies (e.g., via slice/spread or a shallow
map) when setting ai.conversations and the messages array.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2a1df740-de3b-4d45-b1d1-3437d0ca5b8f

📥 Commits

Reviewing files that changed from the base of the PR and between 5c20ae0 and 57a0ac7.

📒 Files selected for processing (1)
  • src/frontend/store/slices/ai/slice.ts

@JoaoGSP JoaoGSP merged commit e7284ae into development May 7, 2026
12 checks passed
@JoaoGSP JoaoGSP deleted the feat/ai-chat-history-persistence-shared branch May 7, 2026 11:37
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