Skip to content

Conversation

@lirielgozi
Copy link

@lirielgozi lirielgozi commented Jan 16, 2026

  • Add ConversationHistory dropdown component with list of past conversations
  • Add useConversations hook for fetching and managing conversations via React Query
  • Implement conversation switching with proper state management
  • Fix bug where reopening panel showed new greeting instead of resuming conversation
  • Fix bug where selecting from history caused conversation ID to revert
  • Add server-side history context loading for resumed conversations
  • Add Playwright E2E tests for conversation history feature
  • Add logging for debugging conversation flow

Key changes:

  • AssistantPanel: manages conversation state with localStorage persistence
  • AssistantChat: header with [+] New Chat and [History] buttons
  • Server: skips greeting for resumed conversations, loads history context on first message
  • Fixed race condition in onConversationCreated callback

Summary by CodeRabbit

  • New Features

    • Conversation history: save, resume, switch, and delete past conversations; New Chat and Conversation History UI with persistence across panel reopens
    • Loading indicator when a conversation is being loaded
  • Improvements

    • Resumed conversations no longer replay initial greeting and include prior messages as context for continuity
  • Tests

    • Added comprehensive end-to-end test suite for conversation history
  • Chores

    • Added end-to-end test config and scripts; improved internal assistant logging

✏️ Tip: You can customize this high-level summary in your review settings.

- Add ConversationHistory dropdown component with list of past conversations
- Add useConversations hook for fetching and managing conversations via React Query
- Implement conversation switching with proper state management
- Fix bug where reopening panel showed new greeting instead of resuming conversation
- Fix bug where selecting from history caused conversation ID to revert
- Add server-side history context loading for resumed conversations
- Add Playwright E2E tests for conversation history feature
- Add logging for debugging conversation flow

Key changes:
- AssistantPanel: manages conversation state with localStorage persistence
- AssistantChat: header with [+] New Chat and [History] buttons
- Server: skips greeting for resumed conversations, loads history context on first message
- Fixed race condition in onConversationCreated callback
@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

This pull request adds conversation history support across backend and frontend: conditional greeting/resume logic and history-aware context on the server, new UI components and hooks for listing/selecting/deleting conversations, localStorage persistence of selected conversation, Playwright E2E tests, and Playwright config & scripts.

Changes

Cohort / File(s) Summary
Ignore patterns
\.gitignore``
Added ignore patterns for Playwright artifacts: ui/playwright-report, ui/test-results.
Backend — WebSocket logging
**server/routers/assistant_chat.py**
Added verbose logging around WebSocket start flow, session creation, chunk sends, and completion (no control-flow changes).
Backend — Session & history handling
\server/services/assistant_chat_session.py``
Added _history_loaded attribute and get_messages import; introduced is_new_conversation path, conditional greeting for new conversations, deferred history loading for resumed conversations, prepends prior messages on first user message, and extra error/debug logging.
Frontend — AssistantChat component
\ui/src/components/AssistantChat.tsx``
Added props for conversation handling (conversationId, initialMessages, isLoadingConversation, onNewChat, onSelectConversation, onConversationCreated), merged initial + live messages with dedupe, header actions (New/History), ConversationHistory integration, loading UI and lifecycle logging.
Frontend — AssistantPanel state
\ui/src/components/AssistantPanel.tsx``
Persist selected conversationId in localStorage, fetch conversation via useConversation, map API messages to ChatMessage, reset on project change, and wire conversation callbacks.
Frontend — ConversationHistory UI
\ui/src/components/ConversationHistory.tsx``
New dropdown component listing conversations (title, count, relative time), select and delete (with ConfirmDialog), Escape/backdrop handling, loading/empty states.
Frontend — React Query hooks
\ui/src/hooks/useConversations.ts``
New hooks: useConversations(projectName), useConversation(projectName, conversationId), and useDeleteConversation(projectName) with cache invalidation/update logic.
Frontend — Assistant WebSocket hook
\ui/src/hooks/useAssistantChat.ts``
Added console logs for incoming WS messages and before sending start; changed clearMessages to preserve conversationId.
E2E tests
\ui/e2e/conversation-history.spec.ts``
New comprehensive Playwright tests (UI-only and Integration) covering panel open/close, UI controls, full conversation lifecycle (send, resume, new chat), history listing, selection, deletion, and keyboard behaviors.
Playwright config & scripts
\ui/playwright.config.ts`, `ui/package.json``
Added Playwright config file and npm scripts test:e2e / test:e2e:ui; added @playwright/test devDependency.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Client UI
    participant Server
    participant DB as Database
    participant Claude

    rect rgba(200,150,255,0.5)
    Note over User,Server: Select existing conversation
    User->>UI: Choose conversation from History
    UI->>Server: GET conversation messages
    Server->>DB: Query conversation & messages
    DB-->>Server: Return messages
    Server-->>UI: Respond with initialMessages
    UI->>UI: Display messages (no greeting)
    end

    rect rgba(150,200,255,0.5)
    Note over User,Claude: First user message for resumed conversation
    User->>UI: Send user message
    UI->>Server: WebSocket start / send message
    Server->>Server: Check _history_loaded flag
    Server->>Claude: Send combined prior-history + new message as context
    Claude-->>Server: Stream response chunks
    Server-->>UI: Stream response chunks to client
    Server->>Server: Set _history_loaded = true
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

"🐰 I bounced through code and fields so green,
I stored the chats where memories convene.
New tests to hop, a history to trace —
Greetings skip, resumed tales embrace. 🥕"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature being added: conversation history functionality for the AI assistant, which is comprehensively implemented across UI components, backend services, and E2E tests.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@lirielgozi
Copy link
Author

image image image image image

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
ui/src/components/AssistantChat.tsx (1)

147-153: Block sending while a conversation is still loading

While isLoadingConversation is true, start() is skipped, so sendMessage can still target the previous conversation. Disable input/send and guard handleSend to prevent misrouted messages.

🔧 Proposed fix
   const handleSend = () => {
     const content = inputValue.trim()
-    if (!content || isLoading) return
+    if (!content || isLoading || isLoadingConversation) return
 
     sendMessage(content)
     setInputValue('')
   }
@@
             placeholder="Ask about the codebase..."
-            disabled={isLoading || connectionStatus !== 'connected'}
+            disabled={isLoading || isLoadingConversation || connectionStatus !== 'connected'}
@@
-            disabled={!inputValue.trim() || isLoading || connectionStatus !== 'connected'}
+            disabled={
+              !inputValue.trim() ||
+              isLoading ||
+              isLoadingConversation ||
+              connectionStatus !== 'connected'
+            }

Also applies to: 291-305

🤖 Fix all issues with AI agents
In `@ui/src/components/AssistantChat.tsx`:
- Around line 60-75: The effect is comparing and storing the prop conversationId
instead of the internal activeConversationId which causes duplicate
onConversationCreated calls when the parent updates conversationId; change the
ref to track activeConversationId (initialize previousConversationIdRef with
activeConversationId and set previousConversationIdRef.current =
activeConversationId inside the useEffect), remove conversationId from the
dependency array, and keep [activeConversationId, onConversationCreated] as the
effect dependencies so onConversationCreated only fires once when the internal
activeConversationId transitions from null/undefined to a value.
- Around line 162-170: displayMessages currently switches from initialMessages
to messages when the first live message arrives, causing initial history loss;
update the logic that computes displayMessages (referencing initialMessages,
messages, lastConversationIdRef, conversationId, isConversationSynced) to merge
both arrays instead of replacing, deduplicating by message ID so any message
present in either source appears once and maintaining order (e.g., keep
initialMessages first then append live messages whose IDs are not already
present) and only fall back to messages when no initialMessages exist or
conversation sync indicates full replacement.
🧹 Nitpick comments (11)
ui/src/hooks/useAssistantChat.ts (1)

122-124: Guard WebSocket debug logs behind a dev flag.

These logs include full payloads; consider gating them to avoid leaking message content and reduce console noise in production.

♻️ Proposed tweak
-        console.log('[useAssistantChat] Received WebSocket message:', data.type, data);
+        if (import.meta.env.DEV) {
+          console.debug('[useAssistantChat] Received WebSocket message:', data.type, data);
+        }
-          console.log('[useAssistantChat] Sending start message:', payload);
+          if (import.meta.env.DEV) {
+            console.debug('[useAssistantChat] Sending start message:', payload);
+          }

Also applies to: 274-282

server/routers/assistant_chat.py (1)

272-288: Avoid info-level logging per streamed chunk.

Per-chunk info logs can be extremely noisy and expensive in production; consider downgrading to debug and gating on log level.

♻️ Suggested adjustment
-                            logger.info(f"Sending chunk: {chunk.get('type')}")
+                            if logger.isEnabledFor(logging.DEBUG):
+                                logger.debug("Sending chunk: %s", chunk.get("type"))
.gitignore (1)

67-67: Normalize gitignore paths (remove leading ./).

The leading ./ prefix in gitignore patterns is redundant. Git normalizes pathnames relative to the .gitignore file without a ./ prefix, making it unnecessary and non-standard. Removing it follows gitignore conventions.

♻️ Proposed changes
-./ui/playwright-report
+ui/playwright-report/
-./ui/test-results
+ui/test-results/

Also applies to: 146-146

ui/src/components/ConversationHistory.tsx (2)

79-82: Remove debug console.log statements before merging.

These logging statements are useful during development but should be removed or converted to conditional debug logging for production code.

🔧 Remove debug logs
  const handleSelectConversation = (conversationId: number) => {
-   console.log('[ConversationHistory] handleSelectConversation called with id:', conversationId)
    onSelectConversation(conversationId)
    onClose()
  }
            {conversations.map((conversation) => {
              const isCurrent = conversation.id === currentConversationId
-             console.log('[ConversationHistory] Rendering conversation:', {
-               id: conversation.id,
-               currentConversationId,
-               isCurrent
-             })

              return (

Also applies to: 132-136


62-72: Consider showing user feedback on delete failure.

Currently, delete errors are only logged to console. Users won't know if deletion failed.

💡 Optional: Add error state or toast notification
  const handleConfirmDelete = async () => {
    if (!conversationToDelete) return

    try {
      await deleteConversation.mutateAsync(conversationToDelete.id)
      setConversationToDelete(null)
    } catch (error) {
      console.error('Failed to delete conversation:', error)
+     // Consider adding a toast notification here
      setConversationToDelete(null)
    }
  }
ui/src/hooks/useConversations.ts (1)

23-29: Consider adding staleTime for single conversation queries.

Unlike useConversations which has a 30-second staleTime, this hook has no staleTime. If the conversation is fetched multiple times in quick succession, it may cause unnecessary API calls.

💡 Optional: Add staleTime for consistency
 export function useConversation(projectName: string | null, conversationId: number | null) {
   return useQuery({
     queryKey: ['conversation', projectName, conversationId],
     queryFn: () => api.getAssistantConversation(projectName!, conversationId!),
     enabled: !!projectName && !!conversationId,
+    staleTime: 30000, // Consistent with useConversations
   })
 }
server/services/assistant_chat_session.py (1)

341-361: History loading logic is sound, but consider edge cases.

The implementation correctly:

  1. Only loads history once via _history_loaded flag
  2. Excludes the just-added message (history[:-1])
  3. Truncates long messages to 500 chars

However, there's no limit on the total number of history messages loaded. For very long conversations, this could result in a large context block.

💡 Consider limiting history messages
         if not self._history_loaded:
             self._history_loaded = True
             history = get_messages(self.project_dir, self.conversation_id)
             # Exclude the message we just added (last one)
             history = history[:-1] if history else []
+            # Limit to last N messages to avoid context overflow
+            MAX_HISTORY_MESSAGES = 20
+            if len(history) > MAX_HISTORY_MESSAGES:
+                history = history[-MAX_HISTORY_MESSAGES:]
             if history:
ui/e2e/conversation-history.spec.ts (2)

26-40: Consider extracting shared helper functions to reduce duplication.

The selectProject, waitForPanelOpen, and waitForPanelClosed functions are duplicated between the two test.describe blocks.

💡 Extract to shared helpers
// At the top of the file, after imports
async function selectProject(page: import('@playwright/test').Page) {
  const projectSelector = page.locator('button:has-text("Select Project")')
  if (await projectSelector.isVisible()) {
    await projectSelector.click()
    const projectItem = page.locator('.neo-dropdown-item').first()
    const hasProject = await projectItem.isVisible().catch(() => false)
    if (!hasProject) return false
    await projectItem.click()
    await page.waitForTimeout(500)
    return true
  }
  return false
}

async function waitForPanelOpen(page: import('@playwright/test').Page) {
  await page.waitForFunction(() => {
    const panel = document.querySelector('[aria-label="Project Assistant"]')
    return panel && panel.getAttribute('aria-hidden') !== 'true'
  }, { timeout: 5000 })
}

async function waitForPanelClosed(page: import('@playwright/test').Page) {
  await page.waitForFunction(() => {
    const panel = document.querySelector('[aria-label="Project Assistant"]')
    return !panel || panel.getAttribute('aria-hidden') === 'true'
  }, { timeout: 5000 })
}

Also applies to: 316-328


36-37: Fixed waitForTimeout calls may cause flakiness.

Multiple fixed waits (500ms, 1000ms, 2000ms) can make tests flaky in CI. Consider using explicit wait conditions where possible.

For example, instead of:

await page.waitForTimeout(500)

Use condition-based waits:

await page.waitForSelector('.expected-element')
// or
await expect(someLocator).toBeVisible()

Also applies to: 324-325, 402-403, 416-417, 444-444, 488-488

ui/src/components/AssistantPanel.tsx (2)

65-70: Remove debug console.log statement before merging.

Similar to ConversationHistory.tsx, this debug logging should be removed for production.

🔧 Remove debug log
-  console.log('[AssistantPanel] State:', {
-    conversationId,
-    isLoadingConversation,
-    conversationDetailId: conversationDetail?.id,
-    initialMessagesCount: initialMessages?.length ?? 0
-  })

73-80: Consider consolidating the two effects to avoid potential timing issues.

Having two separate effects that both depend on projectName could lead to subtle race conditions during project switching. The second effect resets conversationId, but the first effect would have already persisted the old value.

💡 Consolidate effects
-  // Persist conversation ID changes to localStorage
-  useEffect(() => {
-    setStoredConversationId(projectName, conversationId)
-  }, [projectName, conversationId])
-
-  // Reset conversation ID when project changes
-  useEffect(() => {
-    setConversationId(getStoredConversationId(projectName))
-  }, [projectName])
+  // Handle project changes and persist conversation ID
+  const prevProjectRef = useRef(projectName)
+  useEffect(() => {
+    if (prevProjectRef.current !== projectName) {
+      // Project changed - load stored ID for new project
+      prevProjectRef.current = projectName
+      setConversationId(getStoredConversationId(projectName))
+    } else {
+      // Same project - persist current conversation ID
+      setStoredConversationId(projectName, conversationId)
+    }
+  }, [projectName, conversationId])

@lirielgozi lirielgozi changed the title feat: add conversation history feature to AI assistant feature: add conversation history feature to AI assistant Jan 16, 2026
- Fix duplicate onConversationCreated callbacks by tracking activeConversationId
- Fix history loss when switching conversations with Map-based deduplication
- Disable input while conversation is loading to prevent message routing issues
- Gate WebSocket debug logs behind DEV flag (import.meta.env.DEV)
- Downgrade server logging from info to debug level for reduced noise
- Fix .gitignore prefixes for playwright paths (ui/playwright-report/, ui/test-results/)
- Remove debug console.log from ConversationHistory.tsx
- Add staleTime (30s) to single conversation query for better caching
- Increase history message cap from 20 to 35 for better context
- Replace fixed timeouts with condition-based waits in e2e tests
Copy link

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
server/services/assistant_chat_session.py (1)

200-211: Validate resumed conversation IDs before skipping creation.

If a client passes a stale/deleted conversation_id, start() treats it as a resume and skips creation/greeting; subsequent add_message/get_messages then no-op, so the chat runs without persistence or history. Consider verifying the conversation exists and either fall back to a new conversation or return an error. You’ll also need to import get_conversation from assistant_database.

🐛 Suggested fix (validate before treating as resume)
-        # Track if this is a new conversation (for greeting decision)
-        is_new_conversation = self.conversation_id is None
+        # Track if this is a new conversation (for greeting decision)
+        is_new_conversation = self.conversation_id is None
+        if not is_new_conversation:
+            existing = get_conversation(self.project_dir, self.conversation_id)
+            if not existing:
+                logger.warning(
+                    "Conversation %s not found; starting a new one",
+                    self.conversation_id,
+                )
+                self.conversation_id = None
+                is_new_conversation = True
🧹 Nitpick comments (5)
server/services/assistant_chat_session.py (1)

341-368: Consider DB-level limits when loading history.

get_messages returns the full conversation and then slices to the last 35. For long conversations this can be unnecessarily heavy. Consider adding a limit/offset in the query (or a helper like get_recent_messages) to fetch only the last N messages while preserving chronological order.

ui/e2e/conversation-history.spec.ts (3)

26-55: Consider extracting shared test helpers to reduce duplication.

The helper functions selectProject, waitForPanelOpen, and waitForPanelClosed are duplicated verbatim between the two test.describe blocks. This increases maintenance burden when behavior needs to change.

♻️ Suggested refactor

Extract shared helpers to a common scope or a separate file:

+// Shared test helpers
+async function selectProject(page: import('@playwright/test').Page) {
+  const projectSelector = page.locator('button:has-text("Select Project")')
+  if (await projectSelector.isVisible()) {
+    await projectSelector.click()
+    const projectItem = page.locator('.neo-dropdown-item').first()
+    const hasProject = await projectItem.isVisible().catch(() => false)
+    if (!hasProject) return false
+    await projectItem.click()
+    await expect(projectSelector).not.toBeVisible({ timeout: 5000 }).catch(() => {})
+    return true
+  }
+  return false
+}
+
+async function waitForPanelOpen(page: import('@playwright/test').Page) {
+  await page.waitForFunction(() => {
+    const panel = document.querySelector('[aria-label="Project Assistant"]')
+    return panel && panel.getAttribute('aria-hidden') !== 'true'
+  }, { timeout: 5000 })
+}
+
+async function waitForPanelClosed(page: import('@playwright/test').Page) {
+  await page.waitForFunction(() => {
+    const panel = document.querySelector('[aria-label="Project Assistant"]')
+    return !panel || panel.getAttribute('aria-hidden') === 'true'
+  }, { timeout: 5000 })
+}
+
 test.describe('Assistant Panel UI', () => {
-  // ... duplicated helpers removed
+  // Use shared helpers above
 })

Also applies to: 317-344


37-37: Swallowing assertion errors may mask test failures.

The .catch(() => {}) silently ignores the assertion failure. If the dropdown genuinely fails to close, subsequent test steps may produce confusing failures. Consider logging or using a softer wait instead:

-      await expect(projectSelector).not.toBeVisible({ timeout: 5000 }).catch(() => {})
+      // Wait briefly for dropdown animation, but don't fail if it's slow
+      await page.waitForTimeout(500)

484-487: Consider a more specific selector for the confirm dialog button.

Using .last() on button:has-text("Delete") assumes the confirm dialog's button is always last in the DOM. If another Delete button appears, this could click the wrong one.

-    const confirmButton = page.locator('button:has-text("Delete")').last()
+    // Target the confirmation dialog specifically
+    const confirmButton = page.locator('[role="alertdialog"] button:has-text("Delete"), .confirm-dialog button:has-text("Delete")').first()
ui/src/components/AssistantChat.tsx (1)

81-86: Consider gating debug logs behind a development flag.

The console.log statements aid debugging but will appear in production builds. The commit messages mention gating WebSocket debug logs behind a DEV flag—consider applying the same pattern here.

+const DEBUG = import.meta.env.DEV
+
 // In the useEffect:
-    console.log('[AssistantChat] useEffect running:', { ... })
+    if (DEBUG) console.log('[AssistantChat] useEffect running:', { ... })

Also applies to: 90-91, 96-97, 104-108, 115-115, 120-120

pRizz added a commit to pRizz/autocoder that referenced this pull request Jan 21, 2026
…orks

- Verified sidebar Settings link navigates to /settings page
- Settings page displays all configuration options:
  - Model, Provider, Concurrency, YOLO Mode, Testing Agent Ratio
- All steps verified with browser automation
- Screenshots captured for documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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