diff --git a/.github/workflows/ci-e2e-playwright.yml b/.github/workflows/ci-e2e-playwright.yml
index 3bb98eb322cea..e17401297e66e 100644
--- a/.github/workflows/ci-e2e-playwright.yml
+++ b/.github/workflows/ci-e2e-playwright.yml
@@ -41,6 +41,12 @@ env:
PGPASSWORD: posthog
PGPORT: 5432
OIDC_RSA_PRIVATE_KEY: ${{ secrets.OIDC_RSA_PRIVATE_KEY }}
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ INKEEP_API_KEY: ${{ secrets.INKEEP_API_KEY }}
+ AZURE_INFERENCE_CREDENTIAL: ${{ secrets.AZURE_INFERENCE_CREDENTIAL }}
+ AZURE_INFERENCE_ENDPOINT: ${{ secrets.AZURE_INFERENCE_ENDPOINT }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -329,7 +335,7 @@ jobs:
- name: Start PostHog web & Celery worker
run: |
python manage.py run_autoreload_celery --type=worker &> /tmp/celery.log &
- python manage.py runserver 8000 &> /tmp/server.log &
+ python -m granian --interface asgi posthog.asgi:application --host 0.0.0.0 --port 8000 --log-level debug --workers 1 &> /tmp/server.log &
# Install Playwright browsers while we wait for PostHog to be ready
- name: Install Playwright browsers
diff --git a/ee/api/conversation.py b/ee/api/conversation.py
index 4c04e11e0520e..5b29089f84a72 100644
--- a/ee/api/conversation.py
+++ b/ee/api/conversation.py
@@ -111,17 +111,15 @@ class ConversationViewSet(TeamAndOrgViewSetMixin, ListModelMixin, RetrieveModelM
lookup_url_kwarg = "conversation"
def safely_get_queryset(self, queryset):
- # Only allow access to conversations created by the current user
- qs = queryset.filter(user=self.request.user)
-
- # Allow sending messages to any conversation
- if self.action == "create":
- return qs
-
- # But retrieval must only return conversations from the assistant and with a title.
- return qs.filter(
- title__isnull=False, type__in=[Conversation.Type.DEEP_RESEARCH, Conversation.Type.ASSISTANT]
- ).order_by("-updated_at")
+ # Only single retrieval of a specific conversation is allowed for other users' conversations (if ID known)
+ if self.action != "retrieve":
+ queryset = queryset.filter(user=self.request.user)
+ # For listing or single retrieval, conversations must be from the assistant and have a title
+ if self.action in ("list", "retrieve"):
+ queryset = queryset.filter(
+ title__isnull=False, type__in=[Conversation.Type.DEEP_RESEARCH, Conversation.Type.ASSISTANT]
+ ).order_by("-updated_at")
+ return queryset
def get_throttles(self):
if (
diff --git a/ee/api/test/test_conversation.py b/ee/api/test/test_conversation.py
index 06daf42ac282f..9f0998819910d 100644
--- a/ee/api/test/test_conversation.py
+++ b/ee/api/test/test_conversation.py
@@ -156,7 +156,7 @@ def test_add_message_to_existing_conversation(self):
self.assertEqual(str(workflow_inputs.trace_id), trace_id)
self.assertEqual(workflow_inputs.message["content"], "test query")
- def test_cant_access_other_users_conversation(self):
+ def test_cant_start_other_users_conversation(self):
conversation = Conversation.objects.create(user=self.other_user, team=self.team)
self.client.force_login(self.user)
@@ -167,6 +167,18 @@ def test_cant_access_other_users_conversation(self):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ def test_cannot_cancel_other_users_conversation(self):
+ """Test that cancel action cannot use other user's conversation ID"""
+ conversation = Conversation.objects.create(
+ user=self.other_user, team=self.team, title="Other user conversation", type=Conversation.Type.ASSISTANT
+ )
+
+ response = self.client.patch(
+ f"/api/environments/{self.team.id}/conversations/{conversation.id}/cancel/",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
def test_cant_access_other_teams_conversation(self):
conversation = Conversation.objects.create(user=self.user, team=self.other_team)
@@ -313,11 +325,12 @@ def test_cancel_already_canceling_conversation(self):
# should be idempotent
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
- def test_cancel_other_users_conversation(self):
+ def test_cannot_cancel_other_users_conversation_in_same_project(self):
conversation = Conversation.objects.create(user=self.other_user, team=self.team)
response = self.client.patch(
f"/api/environments/{self.team.id}/conversations/{conversation.id}/cancel/",
)
+ # This should fail because cancel action also filters by user
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_cancel_other_teams_conversation(self):
@@ -484,50 +497,63 @@ def test_list_only_returns_assistant_conversations_with_title(self):
self.assertIn("messages", results[0])
self.assertIn("status", results[0])
- def test_retrieve_conversation_without_title_returns_404(self):
- conversation = Conversation.objects.create(
- user=self.user, team=self.team, title=None, type=Conversation.Type.ASSISTANT
+ def test_list_conversations_only_returns_own_conversations(self):
+ """Test that listing conversations only returns the current user's conversations"""
+ # Create conversations for different users
+ own_conversation = Conversation.objects.create(
+ user=self.user, team=self.team, title="My conversation", type=Conversation.Type.ASSISTANT
+ )
+ Conversation.objects.create(
+ user=self.other_user, team=self.team, title="Other user conversation", type=Conversation.Type.ASSISTANT
)
with patch("langgraph.graph.state.CompiledStateGraph.aget_state", new_callable=AsyncMock):
- response = self.client.get(f"/api/environments/{self.team.id}/conversations/{conversation.id}/")
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+ response = self.client.get(f"/api/environments/{self.team.id}/conversations/")
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ results = response.json()["results"]
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]["id"], str(own_conversation.id))
+ self.assertEqual(results[0]["title"], "My conversation")
- def test_retrieve_non_assistant_conversation_returns_404(self):
+ def test_retrieve_own_conversation_succeeds(self):
+ """Test that user can retrieve their own conversation"""
conversation = Conversation.objects.create(
- user=self.user, team=self.team, title="Tool call", type=Conversation.Type.TOOL_CALL
+ user=self.user, team=self.team, title="My conversation", type=Conversation.Type.ASSISTANT
)
with patch("langgraph.graph.state.CompiledStateGraph.aget_state", new_callable=AsyncMock):
response = self.client.get(f"/api/environments/{self.team.id}/conversations/{conversation.id}/")
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.json()["id"], str(conversation.id))
- def test_conversation_serializer_returns_empty_messages_on_validation_error(self):
+ def test_retrieve_other_users_conversation_succeeds(self):
+ """Test that user can retrieve another user's conversation in the same team"""
conversation = Conversation.objects.create(
- user=self.user, team=self.team, title="Conversation with validation error", type=Conversation.Type.ASSISTANT
+ user=self.other_user, team=self.team, title="Other user conversation", type=Conversation.Type.ASSISTANT
)
- # Mock the get_state method to return data that will cause a validation error
- with patch("langgraph.graph.state.CompiledStateGraph.aget_state", new_callable=AsyncMock) as mock_get_state:
-
- class MockSnapshot:
- values = {"invalid_key": "invalid_value"} # Invalid structure for AssistantState
-
- mock_get_state.return_value = MockSnapshot()
-
+ with patch("langgraph.graph.state.CompiledStateGraph.aget_state", new_callable=AsyncMock):
response = self.client.get(f"/api/environments/{self.team.id}/conversations/{conversation.id}/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.json()["id"], str(conversation.id))
- # Should return empty messages array when validation fails
- self.assertEqual(response.json()["messages"], [])
+ def test_retrieve_other_teams_conversation_fails(self):
+ """Test that user cannot retrieve conversation from another team"""
+ conversation = Conversation.objects.create(
+ user=self.user, team=self.other_team, title="Other team conversation", type=Conversation.Type.ASSISTANT
+ )
+
+ with patch("langgraph.graph.state.CompiledStateGraph.aget_state", new_callable=AsyncMock):
+ response = self.client.get(f"/api/environments/{self.team.id}/conversations/{conversation.id}/")
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
- def test_list_conversations_ordered_by_updated_at(self):
- """Verify conversations are listed with most recently updated first"""
+ def test_conversations_ordered_by_updated_at_descending(self):
+ """Test that conversations are ordered by updated_at in descending order"""
# Create conversations with different update times
conversation1 = Conversation.objects.create(
user=self.user, team=self.team, title="Older conversation", type=Conversation.Type.ASSISTANT
)
-
conversation2 = Conversation.objects.create(
user=self.user, team=self.team, title="Newer conversation", type=Conversation.Type.ASSISTANT
)
@@ -535,7 +561,6 @@ def test_list_conversations_ordered_by_updated_at(self):
# Set updated_at explicitly to ensure order
conversation1.updated_at = timezone.now() - datetime.timedelta(hours=1)
conversation1.save()
-
conversation2.updated_at = timezone.now()
conversation2.save()
@@ -546,7 +571,7 @@ def test_list_conversations_ordered_by_updated_at(self):
results = response.json()["results"]
self.assertEqual(len(results), 2)
- # First result should be the newer conversation
+ # First result should be the newer conversation (most recent first)
self.assertEqual(results[0]["id"], str(conversation2.id))
self.assertEqual(results[0]["title"], "Newer conversation")
diff --git a/ee/hogai/api/serializers.py b/ee/hogai/api/serializers.py
index d911dbafeafc2..b68b98c45a2ee 100644
--- a/ee/hogai/api/serializers.py
+++ b/ee/hogai/api/serializers.py
@@ -5,6 +5,7 @@
from langgraph.graph.state import CompiledStateGraph
from rest_framework import serializers
+from posthog.api.shared import UserBasicSerializer
from posthog.exceptions_capture import capture_exception
from ee.hogai.chat_agent.graph import AssistantGraph
@@ -15,7 +16,7 @@
from ee.hogai.utils.types.composed import AssistantMaxGraphState
from ee.models.assistant import Conversation
-_conversation_fields = ["id", "status", "title", "created_at", "updated_at", "type"]
+_conversation_fields = ["id", "status", "title", "user", "created_at", "updated_at", "type"]
MaxGraphType = DeepResearchAssistantGraph | AssistantGraph
@@ -32,8 +33,10 @@ class Meta:
fields = _conversation_fields
read_only_fields = fields
+ user = UserBasicSerializer(read_only=True)
-class ConversationSerializer(serializers.ModelSerializer):
+
+class ConversationSerializer(ConversationMinimalSerializer):
class Meta:
model = Conversation
fields = [*_conversation_fields, "messages", "has_unsupported_content"]
diff --git a/frontend/__snapshots__/scenes-app-max-ai--shared-thread--dark.png b/frontend/__snapshots__/scenes-app-max-ai--shared-thread--dark.png
new file mode 100644
index 0000000000000..bc8c42ee1ac81
Binary files /dev/null and b/frontend/__snapshots__/scenes-app-max-ai--shared-thread--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-max-ai--shared-thread--light.png b/frontend/__snapshots__/scenes-app-max-ai--shared-thread--light.png
new file mode 100644
index 0000000000000..3110550bb81f7
Binary files /dev/null and b/frontend/__snapshots__/scenes-app-max-ai--shared-thread--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--chat-with-ui-context--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--chat-with-ui-context--dark.png
index b9405e2b8625e..da7d9cad100a6 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--chat-with-ui-context--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--chat-with-ui-context--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--chat-with-ui-context--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--chat-with-ui-context--light.png
index 62c4dc81aadf5..fc8031397ddb2 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--chat-with-ui-context--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--chat-with-ui-context--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--empty-thread-loading--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--empty-thread-loading--dark.png
index f8169ee7882c0..812b04f9d3b9d 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--empty-thread-loading--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--empty-thread-loading--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--empty-thread-loading--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--empty-thread-loading--light.png
index 4f643693e1e44..1b898f1df3f55 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--empty-thread-loading--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--empty-thread-loading--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--generation-failure-thread--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--generation-failure-thread--dark.png
index 933f985a26089..13ae32fae04e7 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--generation-failure-thread--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--generation-failure-thread--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--generation-failure-thread--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--generation-failure-thread--light.png
index 9a05600b2719a..9c431c04465b9 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--generation-failure-thread--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--generation-failure-thread--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--max-instance-with-contextual-tools--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--max-instance-with-contextual-tools--dark.png
index 5bd66c084e88f..218b849f0c23c 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--max-instance-with-contextual-tools--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--max-instance-with-contextual-tools--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--max-instance-with-contextual-tools--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--max-instance-with-contextual-tools--light.png
index 7eaa3954b34e0..145c2384bedb8 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--max-instance-with-contextual-tools--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--max-instance-with-contextual-tools--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--multi-visualization-in-thread--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--multi-visualization-in-thread--dark.png
index 31f1f46d78888..4f78fdfc91b81 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--multi-visualization-in-thread--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--multi-visualization-in-thread--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--multi-visualization-in-thread--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--multi-visualization-in-thread--light.png
index 34125140ceedc..c790774a5892b 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--multi-visualization-in-thread--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--multi-visualization-in-thread--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--notebook-update-component--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--notebook-update-component--dark.png
index 26a5545ae6f06..ccb9649ec6d56 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--notebook-update-component--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--notebook-update-component--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--notebook-update-component--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--notebook-update-component--light.png
index e526b62af0d02..23ce79f9d067a 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--notebook-update-component--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--notebook-update-component--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--planning-component--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--planning-component--dark.png
index b695479bc1f2e..15922c030a426 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--planning-component--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--planning-component--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--planning-component--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--planning-component--light.png
index 838c04b241db9..e181309c0f0fa 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--planning-component--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--planning-component--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--reasoning-component--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--reasoning-component--dark.png
index 2c559ae192108..16a43801e6646 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--reasoning-component--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--reasoning-component--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--reasoning-component--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--reasoning-component--light.png
index bcedecf86f07b..e49a68673c0dc 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--reasoning-component--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--reasoning-component--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-empty--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-empty--dark.png
index 6ff5581922af8..de6497d5f00b2 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-empty--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-empty--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-empty--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-empty--light.png
index efa2f09556100..9959d42a5dca8 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-empty--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-empty--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-with-results--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-with-results--dark.png
index 4811e40e6b31e..33aac6224ecab 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-with-results--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-with-results--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-with-results--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-with-results--light.png
index a026d0216ceac..c25809d5ad905 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-with-results--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--search-session-recordings-with-results--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--shared-thread--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--shared-thread--dark.png
new file mode 100644
index 0000000000000..99729cbe063bd
Binary files /dev/null and b/frontend/__snapshots__/scenes-app-posthog-ai--shared-thread--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--shared-thread--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--shared-thread--light.png
new file mode 100644
index 0000000000000..7ede06fe5bb17
Binary files /dev/null and b/frontend/__snapshots__/scenes-app-posthog-ai--shared-thread--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-component--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-component--dark.png
index 839fef99fc838..4863b9073372b 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-component--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-component--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-component--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-component--light.png
index d5ef07d1ab6fa..0bfd254752aea 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-component--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-component--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-with-failure--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-with-failure--dark.png
index 7f907277cbd56..f39fa255f9d0d 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-with-failure--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-with-failure--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-with-failure--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-with-failure--light.png
index 8d6f0fdcfd9b5..e37e6f890e814 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-with-failure--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--task-execution-with-failure--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread--dark.png
index 0ab9e45763960..24aa085ea390a 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread--light.png
index efe24e2a29f6c..272c4ae4a0ad2 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-scrolls-to-bottom-on-new-messages--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-scrolls-to-bottom-on-new-messages--dark.png
index 7f098f3117736..51ce45ce54621 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-scrolls-to-bottom-on-new-messages--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-scrolls-to-bottom-on-new-messages--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-scrolls-to-bottom-on-new-messages--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-scrolls-to-bottom-on-new-messages--light.png
index c7ff9d7e0bb24..9ec05ab48eac2 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-scrolls-to-bottom-on-new-messages--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-scrolls-to-bottom-on-new-messages--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-conversation-loading--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-conversation-loading--dark.png
index 2821f68dccaba..48ca8a4d8c613 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-conversation-loading--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-conversation-loading--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-conversation-loading--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-conversation-loading--light.png
index f78fe64652d17..f263739b8b5e3 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-conversation-loading--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-conversation-loading--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-empty-conversation--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-empty-conversation--dark.png
index 81211138a34dc..5b9afdeba59a9 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-empty-conversation--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-empty-conversation--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-empty-conversation--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-empty-conversation--light.png
index bc6b5fa3a75e3..b32f96e8c4ded 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-empty-conversation--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-empty-conversation--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-failed-generation--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-failed-generation--dark.png
index 64577d14b0877..074ba8e894225 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-failed-generation--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-failed-generation--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-failed-generation--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-failed-generation--light.png
index cd1aff564b748..2f8f3f0dcf79c 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-failed-generation--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-failed-generation--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-form--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-form--dark.png
index a61c6a36dfab0..5452f4d3bd6eb 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-form--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-form--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-form--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-form--light.png
index 895e3e6193dbe..df1b8d918f470 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-form--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-form--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit--dark.png
index e71a57b85eaca..48ccbf47ee5a2 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit--light.png
index 05728610dfa6e..ca8df322f74b4 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit-no-retry-after--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit-no-retry-after--dark.png
index f01cc9756a6f4..8270244cfac70 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit-no-retry-after--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit-no-retry-after--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit-no-retry-after--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit-no-retry-after--light.png
index 71338473e1b39..050799b7c69af 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit-no-retry-after--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-rate-limit-no-retry-after--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-sql-query-overflow--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-sql-query-overflow--dark.png
index 58423b9c2c0fe..3697af1ce1a8d 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-sql-query-overflow--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-sql-query-overflow--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-sql-query-overflow--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-sql-query-overflow--light.png
index bc927a25b8ecc..67d32445b4329 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-sql-query-overflow--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--thread-with-sql-query-overflow--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--welcome--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--welcome--dark.png
index 5358695a7c511..ba9086c0a1dc4 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--welcome--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--welcome--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--welcome--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--welcome--light.png
index 7d4731f9e6204..d89ef565ffe78 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--welcome--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--welcome--light.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--welcome-feature-preview-auto-enrolled--dark.png b/frontend/__snapshots__/scenes-app-posthog-ai--welcome-feature-preview-auto-enrolled--dark.png
index c4bc7736e8957..d261a6ea2dd53 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--welcome-feature-preview-auto-enrolled--dark.png and b/frontend/__snapshots__/scenes-app-posthog-ai--welcome-feature-preview-auto-enrolled--dark.png differ
diff --git a/frontend/__snapshots__/scenes-app-posthog-ai--welcome-feature-preview-auto-enrolled--light.png b/frontend/__snapshots__/scenes-app-posthog-ai--welcome-feature-preview-auto-enrolled--light.png
index f9a3c62c8422b..d722db7173a65 100644
Binary files a/frontend/__snapshots__/scenes-app-posthog-ai--welcome-feature-preview-auto-enrolled--light.png and b/frontend/__snapshots__/scenes-app-posthog-ai--welcome-feature-preview-auto-enrolled--light.png differ
diff --git a/frontend/src/scenes/max/Max.stories.tsx b/frontend/src/scenes/max/Max.stories.tsx
index 49aafe1642f76..e272becdd0474 100644
--- a/frontend/src/scenes/max/Max.stories.tsx
+++ b/frontend/src/scenes/max/Max.stories.tsx
@@ -9,7 +9,7 @@ import {
longResponseChunk,
sqlQueryResponseChunk,
} from './__mocks__/chatResponse.mocks'
-import { MOCK_DEFAULT_ORGANIZATION } from 'lib/api.mock'
+import { MOCK_DEFAULT_BASIC_USER, MOCK_DEFAULT_ORGANIZATION } from 'lib/api.mock'
import { Meta, StoryFn } from '@storybook/react'
import { useActions, useValues } from 'kea'
@@ -62,6 +62,7 @@ const meta: Meta = {
title: 'Test Conversation',
created_at: '2025-04-29T17:44:21.654307Z',
updated_at: '2025-04-29T17:44:29.184791Z',
+ user: MOCK_DEFAULT_BASIC_USER,
messages: [],
},
],
@@ -360,6 +361,62 @@ export const ThreadWithEmptyConversation: StoryFn = () => {
return
}
+export const SharedThread: StoryFn = () => {
+ const sharedConversationId = 'shared-conversation-123'
+
+ useStorybookMocks({
+ get: {
+ '/api/environments/:team_id/conversations/': () => [200, conversationList],
+ [`/api/environments/:team_id/conversations/${sharedConversationId}/`]: () => [
+ 200,
+ {
+ id: sharedConversationId,
+ status: 'idle',
+ title: 'Shared Analysis: User Retention Insights',
+ created_at: '2025-01-15T10:30:00.000000Z',
+ updated_at: '2025-01-15T11:45:00.000000Z',
+ user: {
+ id: 1337, // Different user from MOCK_DEFAULT_BASIC_USER
+ uuid: 'ANOTHER_USER_UUID',
+ email: 'another@test.com',
+ first_name: 'Another',
+ last_name: 'User',
+ },
+ messages: [
+ {
+ id: 'msg-1',
+ content: 'Can you analyze our user retention patterns and suggest improvements?',
+ type: 'human',
+ created_at: '2025-01-15T10:30:00.000000Z',
+ },
+ {
+ id: 'msg-2',
+ content:
+ "I'll analyze your user retention patterns. Let me start by examining your data.\n\nBased on the analysis, I can see several key insights:\n\n1. **Day 1 retention**: 45% of users return the next day\n2. **Week 1 retention**: 28% of users are still active after 7 days\n3. **Month 1 retention**: 15% of users remain engaged after 30 days\n\n**Key findings:**\n- Mobile users have 20% higher retention than desktop users\n- Users who complete onboarding have 3x better retention\n- Peak usage occurs between 6-9 PM local time\n\n**Recommendations:**\n1. Improve onboarding completion rate\n2. Implement mobile-first features\n3. Add engagement features for the 6-9 PM window\n4. Create re-engagement campaigns for users who drop off after day 1",
+ type: 'ai',
+ created_at: '2025-01-15T11:45:00.000000Z',
+ },
+ ],
+ },
+ ],
+ },
+ })
+
+ const { setConversationId } = useActions(maxLogic({ tabId: 'storybook' }))
+
+ useEffect(() => {
+ // Simulate loading a shared conversation via URL parameter
+ setConversationId(sharedConversationId)
+ }, [setConversationId])
+
+ return
+}
+SharedThread.parameters = {
+ testOptions: {
+ waitForLoadersToDisappear: false,
+ },
+}
+
export const ThreadWithInProgressConversation: StoryFn = () => {
useStorybookMocks({
get: {
@@ -594,6 +651,7 @@ export const ChatWithUIContext: StoryFn = () => {
title: 'Event Context Test',
created_at: '2025-04-29T17:44:21.654307Z',
updated_at: '2025-04-29T17:44:29.184791Z',
+ user: MOCK_DEFAULT_BASIC_USER,
messages: [],
},
],
diff --git a/frontend/src/scenes/max/Max.tsx b/frontend/src/scenes/max/Max.tsx
index 2936d4dd8a2df..24955f509d2bf 100644
--- a/frontend/src/scenes/max/Max.tsx
+++ b/frontend/src/scenes/max/Max.tsx
@@ -1,13 +1,23 @@
import { BindLogic, useActions, useValues } from 'kea'
import React from 'react'
-import { IconArrowLeft, IconChevronLeft, IconOpenSidebar, IconPlus, IconSidePanel } from '@posthog/icons'
+import {
+ IconArrowLeft,
+ IconChevronLeft,
+ IconExpand45,
+ IconLock,
+ IconOpenSidebar,
+ IconPlus,
+ IconShare,
+ IconSidePanel,
+} from '@posthog/icons'
import { LemonBanner, LemonTag } from '@posthog/lemon-ui'
import { NotFound } from 'lib/components/NotFound'
import { FEATURE_FLAGS } from 'lib/constants'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
+import { copyToClipboard } from 'lib/utils/copyToClipboard'
import { appLogic } from 'scenes/appLogic'
import { maxGlobalLogic } from 'scenes/max/maxGlobalLogic'
import { sceneLogic } from 'scenes/sceneLogic'
@@ -191,7 +201,7 @@ export const MaxInstance = React.memo(function MaxInstance({
)}
- {!conversationHistoryVisible && !threadVisible && !isAIOnlyMode && (
+ {conversationId && !conversationHistoryVisible && !threadVisible && !isAIOnlyMode && (
+
+