fix(editor): Populate chat history in executions view (#29229)

This commit is contained in:
Suguru Inoue
2026-04-27 12:33:18 +00:00
committed by GitHub
parent 20d9215736
commit cf5cdbfbcf
6 changed files with 257 additions and 67 deletions
@@ -57,7 +57,7 @@ const {
onOverviewPanelResizeEnd,
} = useLogsPanelLayout(workflowName, popOutContainer, popOutContent, container, logsContainer);
const { currentSessionId, messages, refreshSession, displayExecution } = useChatState(
const { currentSessionId, chatOptions, refreshSession, displayExecution } = useChatState(
props.isReadOnly,
);
@@ -168,7 +168,7 @@ function handleChangeOutputTableColumnCollapsing(columnName: string | null) {
>
<div ref="container" :class="$style.container" tabindex="-1">
<N8nResizeWrapper
v-if="hasChat && (!props.isReadOnly || messages.length > 0)"
v-if="hasChat && (!props.isReadOnly || (chatOptions.messageHistory ?? []).length > 0)"
:supported-directions="['right']"
:is-resizing-enabled="isOpen"
:width="chatPanelWidth"
@@ -10,7 +10,7 @@ import {
import { useRootStore } from '@n8n/stores/useRootStore';
import MessageWithButtons from '@n8n/chat/components/MessageWithButtons.vue';
import { chatEventBus } from '@n8n/chat/event-buses';
import type { ChatMessage, ChatOptions, SendMessageResponse } from '@n8n/chat/types';
import type { ChatOptions, SendMessageResponse } from '@n8n/chat/types';
import { v4 as uuid } from 'uuid';
import type { ComputedRef, Ref } from 'vue';
import { computed, ref, toValue, watch } from 'vue';
@@ -26,7 +26,6 @@ import { MessageComponentKey } from '@n8n/chat/constants/messageComponents';
interface ChatState {
currentSessionId: ComputedRef<string>;
messages: ComputedRef<ChatMessage[]>;
previousChatMessages: ComputedRef<string[]>;
refreshSession: () => void;
displayExecution: (executionId: string) => void;
@@ -261,7 +260,7 @@ export function useChatState(
messageComponents: {
[MessageComponentKey.WITH_BUTTONS]: MessageWithButtons,
},
messageHistory: messages.value,
messageHistory: isReadOnly ? restoredChatMessages.value : messages.value,
disabled: ref(isReadOnly),
i18n: {
en: {
@@ -366,9 +365,6 @@ export function useChatState(
return {
currentSessionId: computed(() => logsStore.chatSessionId),
messages: computed(() =>
isReadOnly ? restoredChatMessages.value : logsStore.chatSessionMessages,
),
previousChatMessages,
refreshSession,
displayExecution,
@@ -0,0 +1,120 @@
{
"httpRequest": {
"method": "POST",
"path": "/v1/messages",
"body": {
"contentType": "application/json",
"type": "JSON",
"json": {
"model": "claude-sonnet-4-5-20250929",
"stream": true,
"max_tokens": 8192,
"thinking": {
"type": "disabled"
},
"messages": [
{
"role": "user",
"content": "Hi!"
}
]
},
"rawBytes": "eyJtb2RlbCI6ImNsYXVkZS1zb25uZXQtNC01LTIwMjUwOTI5Iiwic3RyZWFtIjp0cnVlLCJtYXhfdG9rZW5zIjo4MTkyLCJ0aGlua2luZyI6eyJ0eXBlIjoiZGlzYWJsZWQifSwibWVzc2FnZXMiOlt7InJvbGUiOiJ1c2VyIiwiY29udGVudCI6IkhpISJ9XX0="
}
},
"httpResponse": {
"statusCode": 200,
"reasonPhrase": "OK",
"headers": {
"x-envoy-upstream-service-time": [
"1494"
],
"strict-transport-security": [
"max-age=31536000; includeSubDomains; preload"
],
"set-cookie": [
"_cfuvid=JjjjTwlTrrDFqgfRsM9he9mrHuj9XhEgxXbsiBhNeiE-1777281698.7332523-1.0.1.1-OyArYqG84afkWTE3QLvX8_xQHOn90JMyDIjk0jbx7qQ; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com"
],
"request-id": [
"req_011CaU2iSzpUtRdjtvzUYN8p"
],
"cf-cache-status": [
"DYNAMIC"
],
"anthropic-ratelimit-tokens-reset": [
"2026-04-27T09:21:38Z"
],
"anthropic-ratelimit-tokens-remaining": [
"27000000"
],
"anthropic-ratelimit-tokens-limit": [
"27000000"
],
"anthropic-ratelimit-requests-reset": [
"2026-04-27T09:21:38Z"
],
"anthropic-ratelimit-requests-remaining": [
"19998"
],
"anthropic-ratelimit-requests-limit": [
"20000"
],
"anthropic-ratelimit-output-tokens-reset": [
"2026-04-27T09:21:38Z"
],
"anthropic-ratelimit-output-tokens-remaining": [
"4500000"
],
"anthropic-ratelimit-output-tokens-limit": [
"4500000"
],
"anthropic-ratelimit-input-tokens-reset": [
"2026-04-27T09:21:38Z"
],
"anthropic-ratelimit-input-tokens-remaining": [
"22500000"
],
"anthropic-ratelimit-input-tokens-limit": [
"22500000"
],
"X-Robots-Tag": [
"none"
],
"Server": [
"cloudflare"
],
"Date": [
"Mon, 27 Apr 2026 09:21:40 GMT"
],
"Content-Type": [
"text/event-stream; charset=utf-8"
],
"Content-Security-Policy": [
"default-src 'none'; frame-ancestors 'none'"
],
"Cache-Control": [
"no-cache"
],
"CF-RAY": [
"9f2cc2191dd9e527-TXL"
]
},
"cookies": {
"_cfuvid": "JjjjTwlTrrDFqgfRsM9he9mrHuj9XhEgxXbsiBhNeiE-1777281698.7332523-1.0.1.1-OyArYqG84afkWTE3QLvX8_xQHOn90JMyDIjk0jbx7qQ"
},
"body": {
"type": "STRING",
"string": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01Np2KsYNwWYH8AoYtUy4UZA\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":9,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":8,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello! How can I help you today\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"?\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":9,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":12} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n",
"rawBytes": "ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtNS0yMDI1MDkyOSIsImlkIjoibXNnXzAxTnAyS3NZTndXWUg4QW9ZdFV5NFVaQSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwic3RvcF9kZXRhaWxzIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo4LCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCIsImluZmVyZW5jZV9nZW8iOiJub3RfYXZhaWxhYmxlIn19ICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRleHQiLCJ0ZXh0IjoiIn0gIH0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiSGVsbG8hIEhvdyBjYW4gSSBoZWxwIHlvdSB0b2RheSJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6Ij8ifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJzdG9wX2RldGFpbHMiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnMiOjEyfSAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgfQoK",
"contentType": "text/event-stream; charset=utf-8"
}
},
"id": "1777281716392-unknown-host-POST-_v1_messages-3002af3d.json",
"priority": 0,
"timeToLive": {
"unlimited": true
},
"times": {
"unlimited": true
}
}
@@ -0,0 +1,70 @@
import { test as base, expect as baseExpect } from '../../../../../fixtures/base';
import type { CredentialResponse } from '../../../../../services/credential-api-helper';
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY ?? 'mock-anthropic-api-key';
type ExecutionLogFixtures = {
anthropicCredential: CredentialResponse;
anthropicApiKey: string;
proxySetup: undefined;
};
export const executionLogsTestConfig = {
capability: {
services: ['proxy'],
env: {
N8N_COMMUNITY_PACKAGES_ENABLED: 'false', // suppress extraneous HTTP requests
},
},
} as const;
export const test = base.extend<ExecutionLogFixtures>({
anthropicApiKey: async ({}, use) => {
await use(ANTHROPIC_API_KEY);
},
proxySetup: [
async ({ services }, use) => {
// Setup
await services.proxy.clearAllExpectations();
await services.proxy.loadExpectations('execution.logs', { strictBodyMatching: true });
await use(undefined);
// Teardown
if (!process.env.CI) {
await services.proxy.recordExpectations('execution.logs', {
dedupe: true,
transform: (expectation) => {
const response = expectation.httpResponse as {
headers?: Record<string, string[]>;
};
if (response?.headers) {
delete response.headers['anthropic-organization-id'];
}
return expectation;
},
});
}
},
{ auto: true },
],
anthropicCredential: async ({ n8n, anthropicApiKey }, use) => {
const res = await n8n.api.credentials.createCredential({
name: `Anthropic cred ${crypto.randomUUID().slice(0, 8)}`,
type: 'anthropicApi',
data: {
apiKey: anthropicApiKey,
},
});
await use(res);
await n8n.api.credentials.deleteCredential(res.id);
},
});
export const expect = baseExpect;
@@ -1,4 +1,6 @@
import { test, expect } from '../../../../../fixtures/base';
import { test, expect, executionLogsTestConfig } from './fixtures';
test.use(executionLogsTestConfig);
// Node name constants
const NODES = {
@@ -101,58 +103,50 @@ test.describe(
await expect(n8n.canvas.logsPanel.getLogEntries().nth(2)).toContainText(NODES.IF);
});
// TODO: make it possible to test workflows with AI model end-to-end
test.fixme(
'should show input and output data in the selected display mode',
async ({ n8n, setupRequirements }) => {
await setupRequirements({ workflow: 'Workflow_ai_agent.json' });
test('should show input and output data in the selected display mode', async ({
n8n,
anthropicCredential,
setupRequirements,
}) => {
await setupRequirements({ workflow: 'Workflow_ai_agent.json' });
await n8n.canvas.clickZoomToFitButton();
await n8n.canvas.logsPanel.open();
await n8n.canvas.logsPanel.sendManualChatMessage('Hi!');
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
await expect(n8n.canvas.logsPanel.getManualChatMessages().nth(0)).toContainText('Hi!');
await expect(n8n.canvas.logsPanel.getManualChatMessages().nth(1)).toContainText(
'Hello from e2e model!!!',
);
await expect(n8n.canvas.logsPanel.getLogEntries().nth(2)).toHaveText('E2E Chat Model');
await n8n.canvas.logsPanel.getLogEntries().nth(2).click();
await n8n.canvas.clickZoomToFitButton();
await n8n.canvas.openNode('E2E Chat Model');
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(anthropicCredential.name);
await n8n.ndv.close();
await n8n.canvas.logsPanel.open();
await n8n.canvas.logsPanel.sendManualChatMessage('Hi!');
await expect(n8n.canvas.logsPanel.getManualChatMessages().nth(0)).toContainText('Hi!');
await expect(n8n.canvas.logsPanel.getManualChatMessages().nth(1)).toContainText('Hello');
await expect(n8n.canvas.logsPanel.getLogEntries().nth(2)).toHaveText('E2E Chat Model');
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText(
'Hello from e2e model!!!',
);
await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('table');
await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 0)).toContainText(
'text:Hello from **e2e** model!!!',
);
await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 1)).toContainText(
'completionTokens:20',
);
await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('schema');
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText('generations[0]');
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText(
'Hello from **e2e** model!!!',
);
await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('json');
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText(
'[{"response": {"generations": [',
);
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText('Hello');
await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('table');
await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 0)).toContainText('text:Hello');
await expect(n8n.canvas.logsPanel.outputPanel.getTbodyCell(0, 1)).toContainText(
/completionTokens:\d+promptTokens:\d+totalTokens:\d+/,
);
await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('schema');
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText('generations[0]');
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText('Hello');
await n8n.canvas.logsPanel.outputPanel.switchDisplayMode('json');
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText(
/\[\s*{\s*"response":\s*{\s*"generations":\s*\[/,
);
await n8n.canvas.logsPanel.toggleInputPanel();
await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('Human: Hi!');
await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('table');
await expect(n8n.canvas.logsPanel.inputPanel.getTbodyCell(0, 0)).toContainText(
'0:Human: Hi!',
);
await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('schema');
await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('messages[0]');
await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('Human: Hi!');
await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('json');
await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText(
'[{"messages": ["Human: Hi!"],',
);
},
);
await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('Human: Hi!');
await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('table');
await expect(n8n.canvas.logsPanel.inputPanel.getTbodyCell(0, 0)).toContainText(
'0:Human: Hi!',
);
await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('schema');
await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('messages[0]');
await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText('Human: Hi!');
await n8n.canvas.logsPanel.inputPanel.switchDisplayMode('json');
await expect(n8n.canvas.logsPanel.inputPanel.get()).toContainText(
/\[\s*{\s*"messages":\s*\[\s*"Human: Hi!"\s*\],/,
);
});
test('should show input and output data of correct run index and branch', async ({
n8n,
@@ -223,21 +217,25 @@ test.describe(
await expect(n8n.canvas.logsPanel.getLogEntries()).toHaveCount(6);
});
// TODO: make it possible to test workflows with AI model end-to-end
test.fixme('should show logs for a past execution', async ({ n8n, setupRequirements }) => {
test('should show logs for a past execution', async ({
n8n,
anthropicCredential,
setupRequirements,
}) => {
await setupRequirements({ workflow: 'Workflow_ai_agent.json' });
await n8n.canvas.clickZoomToFitButton();
await n8n.canvas.openNode('E2E Chat Model');
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(anthropicCredential.name);
await n8n.ndv.close();
await n8n.canvas.logsPanel.open();
await n8n.canvas.logsPanel.sendManualChatMessage('Hi!');
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
await n8n.notifications.waitForNotificationAndClose('Successful');
await n8n.canvas.openExecutions();
await n8n.executions.getAutoRefreshButton().click();
await expect(n8n.executions.logsPanel.getManualChatMessages().nth(0)).toContainText('Hi!');
await expect(n8n.executions.logsPanel.getManualChatMessages().nth(1)).toContainText(
'Hello from e2e model!!!',
);
await expect(n8n.executions.logsPanel.getManualChatMessages().nth(1)).toContainText('Hello');
await expect(
n8n.executions.logsPanel.getOverviewStatus().filter({ hasText: /Success in [\d.]+m?s/ }),
).toBeVisible();
@@ -23,10 +23,16 @@
},
{
"parameters": {
"response": "Hello from **e2e** model!!!"
"model": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-5-20250929",
"cachedResultName": "Claude Sonnet 4.5"
},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatE2eTest",
"typeVersion": 1,
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [308, 220],
"id": "2f239d5b-95ef-4949-92b6-5a7541e1029f",
"name": "E2E Chat Model"