mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-21 04:37:50 +08:00
feat(editor): Polish Instance AI chat list sidebar (no-changelog) (#29463)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
34d7a02df7
commit
8c0faa27c4
@@ -735,6 +735,7 @@ export interface InstanceAiThreadSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
@@ -503,6 +503,17 @@ export class TypeORMMemoryStorage extends MemoryStorage {
|
||||
return entity;
|
||||
});
|
||||
const saved = await this.messageRepo.save(entities);
|
||||
|
||||
// Bump updatedAt on each parent thread so the chat list orders by last
|
||||
// activity. Without this, an existing thread that receives a new message
|
||||
// keeps its original updatedAt and stays buried in the "Older" group.
|
||||
const threadIds = [
|
||||
...new Set(messages.map((m) => m.threadId).filter((id): id is string => !!id)),
|
||||
];
|
||||
if (threadIds.length > 0) {
|
||||
await this.threadRepo.update(threadIds, { updatedAt: new Date() });
|
||||
}
|
||||
|
||||
return { messages: saved.map((e) => this.entityToMessage(e)) };
|
||||
}
|
||||
|
||||
|
||||
@@ -5133,6 +5133,8 @@
|
||||
"instanceAi.thread.new": "New chat",
|
||||
"instanceAi.sidebar.back": "Back",
|
||||
"instanceAi.sidebar.threads": "Threads",
|
||||
"instanceAi.sidebar.chatHistory": "Chat history",
|
||||
"instanceAi.sidebar.collapse": "Collapse sidebar",
|
||||
"instanceAi.message.reasoning": "Reasoning",
|
||||
"instanceAi.sidebar.noThreads": "No conversations yet",
|
||||
"instanceAi.sidebar.group.thisWeek": "This week",
|
||||
|
||||
@@ -9,16 +9,17 @@ import {
|
||||
useTemplateRef,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
|
||||
import {
|
||||
N8nHeading,
|
||||
N8nIconButton,
|
||||
N8nResizeWrapper,
|
||||
N8nScrollArea,
|
||||
N8nText,
|
||||
N8nButton,
|
||||
N8nTooltip,
|
||||
TOOLTIP_DELAY_MS,
|
||||
} from '@n8n/design-system';
|
||||
import { useLocalStorage, useScroll, useWindowSize } from '@vueuse/core';
|
||||
import { useScroll, useSessionStorage, useWindowSize } from '@vueuse/core';
|
||||
import { N8nCallout } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { InstanceAiAttachment } from '@n8n/api-types';
|
||||
@@ -90,17 +91,20 @@ const displayedMessages = computed(() => store.messages.filter(messageHasVisible
|
||||
const executionTracking = useExecutionPushEvents();
|
||||
|
||||
// --- Header title ---
|
||||
const currentThreadTitle = computed(() => {
|
||||
// Returns the resolved title once we have one, or undefined while we're still
|
||||
// figuring out which thread to show. Rendering only on a defined value avoids
|
||||
// the "New conversation" \u2192 real title flash when resuming a recent thread.
|
||||
const currentThreadTitle = computed<string | undefined>(() => {
|
||||
const thread = store.threads.find((t) => t.id === store.currentThreadId);
|
||||
if (!thread || thread.title === NEW_CONVERSATION_TITLE) {
|
||||
const firstUserMsg = store.messages.find((m) => m.role === 'user');
|
||||
if (firstUserMsg?.content) {
|
||||
const text = firstUserMsg.content.trim();
|
||||
return text.length > 60 ? text.slice(0, 60) + '\u2026' : text;
|
||||
}
|
||||
return NEW_CONVERSATION_TITLE;
|
||||
if (thread && thread.title && thread.title !== NEW_CONVERSATION_TITLE) {
|
||||
return thread.title;
|
||||
}
|
||||
return thread.title;
|
||||
const firstUserMsg = store.messages.find((m) => m.role === 'user');
|
||||
if (firstUserMsg?.content) {
|
||||
const text = firstUserMsg.content.trim();
|
||||
return text.length > 60 ? text.slice(0, 60) + '\u2026' : text;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// --- Canvas / data table preview ---
|
||||
@@ -132,7 +136,8 @@ const showEmptyStateLayout = computed(() => !props.threadId);
|
||||
// Load persisted threads from Mastra storage on mount
|
||||
onMounted(() => {
|
||||
void store.loadThreads().then((loaded) => {
|
||||
if (!loaded || !props.threadId) return;
|
||||
if (!loaded) return;
|
||||
if (!props.threadId) return;
|
||||
// After threads load, validate deep-link: redirect if thread doesn't exist
|
||||
if (!store.threads.some((t) => t.id === props.threadId)) {
|
||||
void router.replace({ name: INSTANCE_AI_VIEW });
|
||||
@@ -179,13 +184,26 @@ const showDebugPanel = ref(false);
|
||||
const isDebugEnabled = computed(() => localStorage.getItem('instanceAi.debugMode') === 'true');
|
||||
|
||||
// --- Sidebar collapse & resize ---
|
||||
const sidebarCollapsed = useLocalStorage('instanceAi.sidebarCollapsed', false);
|
||||
// Session-scoped: survives page refresh, resets when the user navigates away
|
||||
// from the AI chat view (see onBeforeRouteLeave below).
|
||||
const sidebarCollapsed = useSessionStorage('instanceAi.sidebarCollapsed', true);
|
||||
const sidebarWidth = ref(260);
|
||||
|
||||
function toggleSidebarCollapse() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||
}
|
||||
|
||||
// Reset to collapsed when leaving the AI chat namespace, so the next entry
|
||||
// starts collapsed by default. Refreshes (which don't trigger the guard) keep
|
||||
// the user's current open/closed state.
|
||||
const CHAT_ROUTE_NAMES = new Set<string>([INSTANCE_AI_VIEW, INSTANCE_AI_THREAD_VIEW]);
|
||||
onBeforeRouteLeave((to) => {
|
||||
const name = typeof to.name === 'string' ? to.name : undefined;
|
||||
if (!name || !CHAT_ROUTE_NAMES.has(name)) {
|
||||
sidebarCollapsed.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSidebarResize({ width }: { width: number }) {
|
||||
// Drag below min-width threshold → auto-collapse
|
||||
if (width <= 200) {
|
||||
@@ -334,10 +352,10 @@ watch(
|
||||
() => props.threadId,
|
||||
(threadId) => {
|
||||
if (!threadId) {
|
||||
// /instance-ai base route (no :threadId) — reset to a clean empty
|
||||
// state. Without this, `currentThreadId` keeps pointing at the
|
||||
// last thread and the sidebar highlights it alongside the empty
|
||||
// main view (AI-2408). A new thread is created on the first
|
||||
// /instance-ai base route (no :threadId): always show the empty
|
||||
// state. Without this, `currentThreadId` keeps pointing at the last
|
||||
// thread and the sidebar highlights it alongside the empty main
|
||||
// view (AI-2408). A new thread is created on the first
|
||||
// `sendMessage` via `syncThread`.
|
||||
store.clearCurrentThread();
|
||||
return;
|
||||
@@ -403,37 +421,46 @@ function handleStop() {
|
||||
<template>
|
||||
<div :class="$style.container" data-test-id="instance-ai-container">
|
||||
<!-- Resizable sidebar -->
|
||||
<N8nResizeWrapper
|
||||
v-if="!sidebarCollapsed"
|
||||
:class="$style.sidebar"
|
||||
:width="sidebarWidth"
|
||||
:style="{ width: `${sidebarWidth}px` }"
|
||||
:supported-directions="['right']"
|
||||
:is-resizing-enabled="true"
|
||||
:min-width="200"
|
||||
:max-width="400"
|
||||
@resize="handleSidebarResize"
|
||||
>
|
||||
<InstanceAiThreadList />
|
||||
</N8nResizeWrapper>
|
||||
<Transition name="sidebar-slide">
|
||||
<N8nResizeWrapper
|
||||
v-if="!sidebarCollapsed"
|
||||
:class="$style.sidebar"
|
||||
:width="sidebarWidth"
|
||||
:style="{ width: `${sidebarWidth}px` }"
|
||||
:supported-directions="['right']"
|
||||
:is-resizing-enabled="true"
|
||||
:min-width="200"
|
||||
:max-width="400"
|
||||
@resize="handleSidebarResize"
|
||||
>
|
||||
<InstanceAiThreadList @collapse="toggleSidebarCollapse" />
|
||||
</N8nResizeWrapper>
|
||||
</Transition>
|
||||
|
||||
<!-- Main chat area -->
|
||||
<div :class="$style.chatArea">
|
||||
<!-- Header -->
|
||||
<div :class="$style.header">
|
||||
<N8nButton
|
||||
:icon="sidebarCollapsed ? 'list' : 'panel-left'"
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
data-test-id="instance-ai-sidebar-toggle"
|
||||
:icon-only="!sidebarCollapsed"
|
||||
@click="toggleSidebarCollapse"
|
||||
>
|
||||
<template v-if="sidebarCollapsed">{{
|
||||
i18n.baseText('instanceAi.sidebar.threads')
|
||||
}}</template>
|
||||
</N8nButton>
|
||||
<N8nHeading tag="h2" size="small" :class="$style.headerTitle">
|
||||
<Transition name="sidebar-toggle-fade">
|
||||
<span v-if="sidebarCollapsed" :class="$style.sidebarToggle">
|
||||
<N8nTooltip
|
||||
:content="i18n.baseText('instanceAi.sidebar.chatHistory')"
|
||||
placement="bottom"
|
||||
:show-after="TOOLTIP_DELAY_MS"
|
||||
>
|
||||
<N8nIconButton
|
||||
icon="history"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
icon-size="large"
|
||||
data-test-id="instance-ai-sidebar-toggle"
|
||||
:aria-label="i18n.baseText('instanceAi.sidebar.chatHistory')"
|
||||
@click="toggleSidebarCollapse"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
</span>
|
||||
</Transition>
|
||||
<N8nHeading v-if="currentThreadTitle" tag="h2" size="small" :class="$style.headerTitle">
|
||||
{{ currentThreadTitle }}
|
||||
</N8nHeading>
|
||||
<N8nText
|
||||
@@ -455,8 +482,8 @@ function handleStop() {
|
||||
<N8nIconButton
|
||||
icon="cog"
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
:class="$style.settingsButton"
|
||||
size="small"
|
||||
icon-size="large"
|
||||
data-test-id="instance-ai-settings-button"
|
||||
@click="goToSettings"
|
||||
/>
|
||||
@@ -464,7 +491,8 @@ function handleStop() {
|
||||
v-if="isDebugEnabled"
|
||||
icon="bug"
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
size="small"
|
||||
icon-size="large"
|
||||
:class="{ [$style.activeButton]: showDebugPanel }"
|
||||
@click="
|
||||
showDebugPanel = !showDebugPanel;
|
||||
@@ -475,7 +503,8 @@ function handleStop() {
|
||||
v-if="!preview.isPreviewVisible.value"
|
||||
icon="panel-right"
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
size="small"
|
||||
icon-size="large"
|
||||
@click="showArtifactsPanel = !showArtifactsPanel"
|
||||
/>
|
||||
</div>
|
||||
@@ -717,11 +746,11 @@ function handleStop() {
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: var(--spacing--sm) var(--spacing--lg);
|
||||
padding: var(--spacing--2xs) var(--spacing--xs);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--xs);
|
||||
gap: var(--spacing--2xs);
|
||||
background-color: var(--color--background--light-2);
|
||||
}
|
||||
|
||||
@@ -740,8 +769,8 @@ function handleStop() {
|
||||
gap: var(--spacing--4xs);
|
||||
}
|
||||
|
||||
.settingsButton {
|
||||
padding: var(--spacing--xs);
|
||||
.sidebarToggle {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.activeButton {
|
||||
@@ -877,4 +906,36 @@ function handleStop() {
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-slide-enter-active,
|
||||
.sidebar-slide-leave-active {
|
||||
transition:
|
||||
width 0.2s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
min-width 0.2s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
opacity 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-slide-enter-from,
|
||||
.sidebar-slide-leave-to {
|
||||
width: 0 !important;
|
||||
min-width: 0 !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// Entry-point icon button: fade in slightly after the sidebar has begun
|
||||
// collapsing, fade out quickly when the sidebar starts opening — so the
|
||||
// crossover feels intentional rather than abrupt.
|
||||
.sidebar-toggle-fade-enter-from,
|
||||
.sidebar-toggle-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar-toggle-fade-enter-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-toggle-fade-leave-active {
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
+61
-34
@@ -2,10 +2,11 @@
|
||||
import { getRelativeDate } from '@/features/ai/chatHub/chat.utils';
|
||||
import {
|
||||
N8nActionDropdown,
|
||||
N8nIcon,
|
||||
N8nIconButton,
|
||||
N8nText,
|
||||
N8nScrollArea,
|
||||
N8nTooltip,
|
||||
TOOLTIP_DELAY_MS,
|
||||
} from '@n8n/design-system';
|
||||
import type { ActionDropdownItem } from '@n8n/design-system/types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
@@ -14,6 +15,8 @@ import { useRouter } from 'vue-router';
|
||||
import { INSTANCE_AI_VIEW, INSTANCE_AI_THREAD_VIEW } from '../constants';
|
||||
import { useInstanceAiStore } from '../instanceAi.store';
|
||||
|
||||
const emit = defineEmits<{ collapse: [] }>();
|
||||
|
||||
const store = useInstanceAiStore();
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
@@ -47,8 +50,12 @@ const groupedThreads = computed(() => {
|
||||
const now = new Date();
|
||||
const groups = new Map<string, typeof store.threads>();
|
||||
|
||||
// Group by last activity, not creation date — a thread created weeks ago
|
||||
// but messaged today belongs under "Today", matching the backend ordering
|
||||
// (memory.service returns threads sorted by updatedAt desc) and the
|
||||
// chatHub sidebar's `groupConversationsByDate` behaviour.
|
||||
for (const thread of store.threads) {
|
||||
const group = getRelativeDate(now, thread.createdAt);
|
||||
const group = getRelativeDate(now, thread.updatedAt ?? thread.createdAt);
|
||||
if (!groups.has(group)) {
|
||||
groups.set(group, []);
|
||||
}
|
||||
@@ -115,17 +122,44 @@ function handleThreadAction(action: string, threadId: string) {
|
||||
|
||||
<template>
|
||||
<div :class="$style.container" data-test-id="instance-ai-thread-list">
|
||||
<!-- New chat button -->
|
||||
<button
|
||||
:class="$style.newChatButton"
|
||||
data-test-id="instance-ai-new-thread-button"
|
||||
@click="handleNewThread"
|
||||
>
|
||||
<div :class="$style.newChatIcon">
|
||||
<N8nIcon icon="plus" size="medium" />
|
||||
<!-- Sidebar header -->
|
||||
<div :class="$style.header">
|
||||
<N8nText :class="$style.title" tag="div" size="medium" bold>
|
||||
{{ i18n.baseText('instanceAi.sidebar.chatHistory') }}
|
||||
</N8nText>
|
||||
<div :class="$style.headerActions">
|
||||
<N8nTooltip
|
||||
:content="i18n.baseText('instanceAi.sidebar.collapse')"
|
||||
placement="bottom"
|
||||
:show-after="TOOLTIP_DELAY_MS"
|
||||
>
|
||||
<N8nIconButton
|
||||
icon="chevrons-left"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
icon-size="large"
|
||||
:aria-label="i18n.baseText('instanceAi.sidebar.collapse')"
|
||||
data-test-id="instance-ai-sidebar-collapse"
|
||||
@click="emit('collapse')"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
<N8nTooltip
|
||||
:content="i18n.baseText('instanceAi.thread.new')"
|
||||
placement="bottom"
|
||||
:show-after="TOOLTIP_DELAY_MS"
|
||||
>
|
||||
<N8nIconButton
|
||||
icon="plus"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
icon-size="large"
|
||||
:aria-label="i18n.baseText('instanceAi.thread.new')"
|
||||
data-test-id="instance-ai-new-thread-button"
|
||||
@click="handleNewThread"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
{{ i18n.baseText('instanceAi.thread.new') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Thread list -->
|
||||
<N8nScrollArea :class="$style.threadList">
|
||||
@@ -199,34 +233,27 @@ function handleThreadAction(action: string, threadId: string) {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.newChatButton {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--xs);
|
||||
padding: var(--spacing--sm);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size--sm);
|
||||
font-weight: var(--font-weight--bold);
|
||||
color: var(--color--text);
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: var(--color--background--light-1);
|
||||
}
|
||||
gap: var(--spacing--3xs);
|
||||
padding: var(--spacing--2xs) var(--spacing--3xs) var(--spacing--2xs) var(--spacing--sm);
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.newChatIcon {
|
||||
.title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--color--text);
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--color--primary);
|
||||
color: white;
|
||||
gap: var(--spacing--5xs);
|
||||
}
|
||||
|
||||
.threadList {
|
||||
|
||||
@@ -640,6 +640,7 @@ export const useInstanceAiStore = defineStore('instanceAi', () => {
|
||||
id: t.id,
|
||||
title: t.title || NEW_CONVERSATION_TITLE,
|
||||
createdAt: t.createdAt,
|
||||
updatedAt: t.updatedAt,
|
||||
metadata: t.metadata ?? undefined,
|
||||
}));
|
||||
threads.value = [...localOnly, ...serverThreads];
|
||||
@@ -659,6 +660,7 @@ export const useInstanceAiStore = defineStore('instanceAi', () => {
|
||||
const existingThread = threads.value.find((thread) => thread.id === threadId);
|
||||
if (existingThread) {
|
||||
existingThread.createdAt = result.thread.createdAt;
|
||||
existingThread.updatedAt = result.thread.updatedAt;
|
||||
existingThread.title = result.thread.title || existingThread.title;
|
||||
return;
|
||||
}
|
||||
@@ -667,6 +669,7 @@ export const useInstanceAiStore = defineStore('instanceAi', () => {
|
||||
id: result.thread.id,
|
||||
title: result.thread.title || NEW_CONVERSATION_TITLE,
|
||||
createdAt: result.thread.createdAt,
|
||||
updatedAt: result.thread.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -318,6 +318,11 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
||||
setCurrentProject(null);
|
||||
}
|
||||
|
||||
if (newRoute?.path?.includes('instance-ai')) {
|
||||
projectNavActiveId.value = 'instance-ai';
|
||||
setCurrentProject(null);
|
||||
}
|
||||
|
||||
if (newRoute?.path?.includes('workflow/')) {
|
||||
if (currentProjectId.value) {
|
||||
projectNavActiveId.value = currentProjectId.value;
|
||||
|
||||
@@ -19,6 +19,26 @@ export class InstanceAiPage extends BasePage {
|
||||
return this.page.getByTestId('instance-ai-container');
|
||||
}
|
||||
|
||||
getSidebarToggle(): Locator {
|
||||
return this.getContainer().getByTestId('instance-ai-sidebar-toggle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the chat-history sidebar if it isn't already open. The sidebar
|
||||
* starts collapsed by default, so any test that needs to query thread
|
||||
* items must open it first. Idempotent — does nothing if already open.
|
||||
*
|
||||
* Waits for the thread-list to become visible so callers can immediately
|
||||
* query thread items without racing the 200ms slide-in transition.
|
||||
*/
|
||||
async openSidebar(): Promise<void> {
|
||||
const toggle = this.getSidebarToggle();
|
||||
if (await toggle.isVisible()) {
|
||||
await toggle.click();
|
||||
}
|
||||
await this.getContainer().getByTestId('instance-ai-thread-list').waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
// ── Messages ──────────────────────────────────────────────────────
|
||||
|
||||
getChatInput(): Locator {
|
||||
|
||||
@@ -45,6 +45,9 @@ test.describe(
|
||||
await n8n.navigate.toInstanceAi();
|
||||
await expect(n8n.instanceAi.getChatInput()).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Sidebar starts collapsed; open it so the thread list is queryable.
|
||||
await n8n.instanceAi.openSidebar();
|
||||
|
||||
// The old thread should be visible in the sidebar. Its title is generated by the model,
|
||||
// so restore it by position rather than by exact title text.
|
||||
const oldThread = n8n.instanceAi.sidebar.getThreadItems().first();
|
||||
|
||||
@@ -15,6 +15,9 @@ test.describe(
|
||||
await n8n.instanceAi.sendMessage('First thread message');
|
||||
await n8n.instanceAi.waitForResponseComplete();
|
||||
|
||||
// Sidebar starts collapsed; open it so the thread list is queryable.
|
||||
await n8n.instanceAi.openSidebar();
|
||||
|
||||
const threadCountBefore = await n8n.instanceAi.sidebar.getThreadItems().count();
|
||||
|
||||
// Click new thread button
|
||||
@@ -40,6 +43,10 @@ test.describe(
|
||||
await n8n.instanceAi.sendMessage('Message in first thread');
|
||||
await n8n.instanceAi.waitForResponseComplete();
|
||||
|
||||
// Sidebar starts collapsed; open it so the new-thread button and
|
||||
// thread list are queryable.
|
||||
await n8n.instanceAi.openSidebar();
|
||||
|
||||
// Create second thread
|
||||
await n8n.instanceAi.sidebar.getNewThreadButton().click();
|
||||
await expect(n8n.instanceAi.getChatInput()).toBeVisible({ timeout: 10_000 });
|
||||
@@ -65,6 +72,9 @@ test.describe(
|
||||
await n8n.instanceAi.sendMessage('Thread to rename');
|
||||
await n8n.instanceAi.waitForResponseComplete();
|
||||
|
||||
// Sidebar starts collapsed; open it so the thread list is queryable.
|
||||
await n8n.instanceAi.openSidebar();
|
||||
|
||||
// Double-click the thread to enter rename mode
|
||||
const thread = n8n.instanceAi.sidebar.getThreadItems().first();
|
||||
await thread.dblclick();
|
||||
@@ -88,6 +98,9 @@ test.describe(
|
||||
await n8n.instanceAi.sendMessage('Thread to delete');
|
||||
await n8n.instanceAi.waitForResponseComplete();
|
||||
|
||||
// Sidebar starts collapsed; open it so the thread list is queryable.
|
||||
await n8n.instanceAi.openSidebar();
|
||||
|
||||
// Verify target thread is visible in the sidebar. Its generated title is not part of
|
||||
// the behavior under test, so use the current thread item instead of title text.
|
||||
const targetThread = n8n.instanceAi.sidebar.getThreadItems().first();
|
||||
|
||||
Reference in New Issue
Block a user