Skip to content

Global: AI Drawer chat history - #4035

Open
finnar-bin wants to merge 76 commits into
devfrom
enhancement/4031-ai-drawer-chat-history
Open

Global: AI Drawer chat history#4035
finnar-bin wants to merge 76 commits into
devfrom
enhancement/4031-ai-drawer-chat-history

Conversation

@finnar-bin

@finnar-bin finnar-bin commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Resolves #4031
Resolves #4134

Summary

  • AI chat is now backed by persistent, per-page chat history stored server-side instead of a single ephemeral thread — creating a session and logging prompts goes through the /client API, with new getChatSessions, getChatSessionLog, and updatePromptApprovalStatus endpoints on mcpApi.
  • Opening the drawer on a page with prior conversations shows a searchable chat history list instead of always starting blank; picking a row loads that session, "New Chat" forces a fresh composer, and a back button returns to history without losing the open thread.
  • "Generate Suggestions" is now scoped to a brand-new, fully empty thread instead of showing on every message.
  • Auto-apply, manual "Apply", and "Clear Chat" now sync prompt approval status with the backend; "Clear Chat" returns to chat history (not a blank thread) when other sessions exist for the page.
  • Block generation responses now resolve blocks extracted from a ZUID, and the drawer no longer gets pushed off-screen when open on the Code Editor.
  • Restricted the AI drawer toggle in the global topbar to pages that actually support it (content editor, content meta, blocks, code editor).
  • Fixed two crashes (Cannot read properties of undefined (reading 'map')) where setResponses updaters assumed a prompt's response array still existed after the active chat session was switched or cleared mid-flight.
  • Refactored src/shell/views/Shell/AIDrawer.tsx (a single 779-line file) into src/shell/components/AIDrawer/, split by concern (index.tsx, ChatThread.tsx, ChatHistory.tsx, PromptComposer.tsx, AnimatedText.tsx, GeneratedImage.tsx).

Dependency

Test plan

  • Open the AI drawer on a page with no chat history — confirm the loading state then an empty composer with "Generate Suggestions".
  • Click "Generate Suggestions", pick a suggestion, and send it — confirm it becomes the thread's first message and the button no longer shows.
  • Send a prompt, reload the page, and confirm the chat and its responses persist.
  • Generate text, image, and block content and confirm each applies correctly to its target field.
  • Toggle auto-apply on, generate content, and confirm it applies without manual approval (then toggle it back off).
  • Clear an existing chat and confirm it returns to chat history (not a blank composer) when other sessions exist for the page.
  • Search chat history by a non-matching term and confirm the empty-results message shows without losing other sessions once cleared.
  • Start a new chat via "New Chat", confirm it opens an empty composer with a back button, and that it creates a second, independent session.
  • From chat history, click each session's row and confirm it loads that session's own conversation, not another session's.
  • Open the AI drawer in the Code Editor, generate code, and confirm the drawer stays fully visible (not pushed off-screen).
  • cypress/e2e/shell/ai-drawer.spec.js passes end-to-end.

Screenshots

Screen.Recording.2026-08-18.140538.mov

and ensured that prompt approval status is updated when a prompt is
auto-applied
@finnar-bin finnar-bin self-assigned this Apr 14, 2026
Comment thread src/shell/components/AIDrawer/ChatHistory.tsx
Comment thread src/shell/components/AIDrawer/index.tsx
Comment thread src/shell/components/AIDrawer/index.tsx
Comment thread src/shell/components/AIDrawer/index.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 4 warning(s) — see inline comments

latestPromptZUIDs.values().next().value only ever picked one entry out
of the set, so a log delta surfacing more than one new prompt ZUID at
once silently skipped auto-applying the rest. Iterate the whole set and
batch the resulting optimistic approval update into a single
setResponses call.
Comment thread src/shell/components/AIDrawer/ChatThread.tsx
Comment thread src/shell/services/mcp.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 2 warning(s) — see inline comments

Comment thread src/shell/components/AIDrawer/index.tsx
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 1 warning(s) — see inline comments

Comment thread src/shell/components/AIDrawer/ChatHistory.tsx
Comment thread src/shell/components/AIDrawer/index.tsx
Comment thread src/shell/components/AIDrawer/index.tsx
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 3 warning(s) — see inline comments

Comment thread src/shell/components/AIDrawer/index.tsx
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 1 warning(s) — see inline comments

Comment thread src/shell/components/AIDrawer/index.tsx
Comment thread src/shell/services/mcp.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 2 warning(s) — see inline comments

? `Generate suggestions: ${normalizedPrompt}`
: "Generate suggestions for my content fields";

isAwaitingLiveResponseRef.current = true;

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.

🟡 handleGenerateSuggestions fires without roleZuid when roles haven't loaded, silently creating an orphaned session

handlePrompt (line 383) guards with if (!newPrompt?.trim() || !userRole?.ZUID) return; before calling geminiGenerate. handleGenerateSuggestions has no such guard: when !urlChatZUID && !userRole?.ZUID (roles still loading), the spread on line 459 emits no roleZuid, yet isAwaitingLiveResponseRef.current is already set to true and pendingPrompt is optimistically rendered. If the backend requires roleZuid to create a new session, the API call fails silently—the loading indicator never clears and the pending message stays stuck.

Suggested change
isAwaitingLiveResponseRef.current = true;
if (!urlChatZUID && !userRole?.ZUID) return;
isAwaitingLiveResponseRef.current = true;

);
}, [sessions, searchTerm]);

const handleSearch = useMemo(

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.

🟡 Debounced function not cancelled on unmount — pending timer fires against detached state

debounce returns a function with an internal timer. useMemo with [] creates it once but never schedules cleanup. If ChatHistory unmounts while a keystroke is still in the 300 ms window (e.g., the user navigates away mid-typing), lodash fires the callback and calls setSearchTerm on the old, now-detached component instance. React 18 silences the warning but the stale state update can produce surprising results if the component remounts quickly.

Suggested change
const handleSearch = useMemo(
const handleSearch = useMemo(
() => debounce((term: string) => setSearchTerm(term), 300),
[]
);
useEffect(() => () => handleSearch.cancel(), [handleSearch]);

{response.payload?.value?.startsWith("3-") ? (
<GeneratedImage src={response.payload.value} />
) : (
<AnimatedText

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.

🟡 AnimatedText crashes when response.payload.value is undefined and animate is true

response.payload.value is typed as any (comes from server JSON). If the AI backend ever returns a response object whose payload lacks a value key (e.g. a new action type, a partial response, or a regression in the MCP API), text is undefined. AnimatedText's interval then accesses text.length and throws TypeError: Cannot read properties of undefined (reading 'length'), crashing the component tree.

The ternary on line 277 uses optional chaining (response.payload?.value?.startsWith) so it safely routes undefined into this branch rather than the image branch, making the crash reachable.

Fix: guard the text prop at the call site or inside AnimatedText.

Suggested change
<AnimatedText
<AnimatedText
key={`${promptZUID}-${responseIndex}`}
text={response.payload.value ?? ""}
animate={shouldAnimate && !isInCodeApp}

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 3 warning(s) — see inline comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Manager UI - AI Chat Threads AI Drawer - Chat History

5 participants