From bbef81274c7630eeb0fd4155f905106a9220d44f Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 5 Jun 2026 15:05:40 +0100 Subject: [PATCH] Show recent Assistant sessions in empty chat --- .../v6/internal/subsystems/ai-runtime.md | 4 + .../src/components/AI/Chat/ChatMessages.tsx | 55 +++++++++++++- .../AI/Chat/__tests__/ChatMessages.test.tsx | 74 ++++++++++++++++++- 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 3b6c1ab7f..82ca9687b 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -108,6 +108,10 @@ runtime cost control, and shared AI transport surfaces. persisted tool calls into the same transcript event shape used by live streams so switching sessions does not hide prior tool evidence or collapse the resumed conversation into a text-only transcript. + The empty Assistant drawer may surface recent non-empty sessions as direct + resume actions using the backend session list already owned by the drawer; + it must not create a parallel recent-chat store or product-authored prompt + shortcut path. Assistant output hygiene is part of the same boundary: provider reasoning and raw serialized tool-call artifacts must never render as assistant transcript prose. Reasoning/thinking deltas may update neutral progress diff --git a/frontend-modern/src/components/AI/Chat/ChatMessages.tsx b/frontend-modern/src/components/AI/Chat/ChatMessages.tsx index dfb81e47b..cf264c5cd 100644 --- a/frontend-modern/src/components/AI/Chat/ChatMessages.tsx +++ b/frontend-modern/src/components/AI/Chat/ChatMessages.tsx @@ -2,6 +2,7 @@ import { Component, Show, For, createEffect, createMemo } from 'solid-js'; import { MessageItem } from './MessageItem'; import type { ChatSession } from '@/api/aiChat'; import type { ChatMessage, PendingApproval, PendingQuestion } from './types'; +import { humanizeToken } from '@/utils/textPresentation'; interface ChatMessagesProps { messages: ChatMessage[]; @@ -50,6 +51,23 @@ export const ChatMessages: Component = (props) => { ); }); + const recentSessions = createMemo(() => + (props.recentSessions || []).filter((session) => session.message_count > 0).slice(0, 3), + ); + + const formatSessionMessageCount = (count: number) => + `${count} ${count === 1 ? 'message' : 'messages'}`; + + const formatSessionHandoffLabel = (session: ChatSession) => { + const summary = session.handoff_summary; + if (!summary) return ''; + const kind = summary.kind?.trim(); + if (kind) return humanizeToken(kind); + if (summary.finding_id) return 'Patrol finding'; + if (summary.run_id) return 'Patrol run'; + return summary.has_model_context ? 'Context attached' : ''; + }; + // Auto-scroll to bottom on new messages or streaming content createEffect(() => { // Access the trigger to establish dependency (void suppresses unused var warning) @@ -73,11 +91,46 @@ export const ChatMessages: Component = (props) => {
{/* Empty state */} -
+

{props.emptyState!.title}

{props.emptyState!.subtitle}

+ 0 && props.onLoadSession}> +
+
Recent sessions
+
+ + {(session) => { + const handoffLabel = () => formatSessionHandoffLabel(session); + return ( + + ); + }} + +
+
+
diff --git a/frontend-modern/src/components/AI/Chat/__tests__/ChatMessages.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/ChatMessages.test.tsx index c03301246..23e1545b3 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/ChatMessages.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/ChatMessages.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it, vi, afterEach, beforeEach } from 'vitest'; -import { cleanup, render, screen } from '@solidjs/testing-library'; +import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library'; import { ChatMessages } from '../ChatMessages'; import type { ChatMessage, PendingApproval, PendingQuestion } from '../types'; @@ -118,6 +118,78 @@ describe('ChatMessages', () => { expect(screen.queryByText('Welcome')).not.toBeInTheDocument(); expect(screen.queryByText('Try asking')).not.toBeInTheDocument(); }); + + it('shows recent sessions as resume actions in the empty state', () => { + const onLoadSession = vi.fn(); + render(() => ( + + )); + + expect(screen.getByLabelText('Recent Assistant sessions')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Resume Storage follow-up' })).toHaveTextContent( + '4 messages', + ); + expect(screen.getByRole('button', { name: 'Resume Storage follow-up' })).toHaveTextContent( + 'Patrol Finding', + ); + expect(screen.getByRole('button', { name: 'Resume Router question' })).toHaveTextContent( + '1 message', + ); + + fireEvent.click(screen.getByRole('button', { name: 'Resume Storage follow-up' })); + expect(onLoadSession).toHaveBeenCalledWith('session-1'); + }); + + it('does not show recent session resume actions when messages are present', () => { + render(() => ( + + )); + + expect(screen.queryByLabelText('Recent Assistant sessions')).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'Resume Storage follow-up' }), + ).not.toBeInTheDocument(); + }); }); describe('message rendering', () => {