Repository: n8n-io/n8n
Pull request #14045: feat(editor): Logs overview panel
Author: login: autologie
Labels: name: n8n team
description: Authored by the n8n team
name: Released
Created: 2025-03-19T09:49:50Z | Merged: 2025-03-31T11:19:55Z

## Summary
This PR implements the new design of logs overview panel behind feature flag.

![Screenshot from 2025-03-27 16-06-34](https://github.com/user-attachments/assets/8ed1d76b-687a-4132-b6eb-5adb53580593)

To try out, set local storage key `N8N_LOGS_2025_SPRING` to `true` and refresh the browser.
Note that the logs overview works only for workflows with AI agent node for now.

## Related Linear tickets, Github issues, and Community forum posts
https://linear.app/n8n/issue/SUG-23/feature-logs-overview-panel

## Review / Merge checklist

- [x] PR title and summary are descriptive. ([conventions](../blob/master/.github/pull_request_title_conventions.md)) <!--
   **Remember, the title automatically goes into the changelog.
   Use `(no-changelog)` otherwise.**
-->
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created.
- [x] Tests included. <!--
   A bug is not considered fixed, unless a test is added to prevent it from happening again.
   A feature is not complete without tests.
-->
- [ ] PR Labeled with `release/backport` (if the PR is an urgent fix that needs to be backported)



Diff:
diff --git a/packages/frontend/@n8n/chat/src/components/MessagesList.vue b/packages/frontend/@n8n/chat/src/components/MessagesList.vue
index a7c8a87119cd6..a0ca0becdf239 100644
--- a/packages/frontend/@n8n/chat/src/components/MessagesList.vue
+++ b/packages/frontend/@n8n/chat/src/components/MessagesList.vue
@@ -88,7 +88,7 @@ watch(
 	justify-content: center;
 	gap: var(--spacing-xs);
 	padding-inline: var(--spacing-m);
-	padding-bottom: 1.5em;
+	padding-bottom: var(--spacing-l);
 	overflow: hidden;
 }
 
diff --git a/packages/frontend/@n8n/design-system/package.json b/packages/frontend/@n8n/design-system/package.json
index aabcc2b90bf30..4d46ea8a7d575 100644
--- a/packages/frontend/@n8n/design-system/package.json
+++ b/packages/frontend/@n8n/design-system/package.json
@@ -49,7 +49,7 @@
     "@fortawesome/fontawesome-svg-core": "^1.2.36",
     "@fortawesome/free-solid-svg-icons": "^5.15.4",
     "@fortawesome/vue-fontawesome": "^3.0.3",
-    "element-plus": "2.4.3",
+    "element-plus": "catalog:frontend",
     "is-emoji-supported": "^0.0.5",
     "markdown-it": "^13.0.2",
     "markdown-it-emoji": "^2.0.2",
diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json
index 4ee373c549680..0a97069c13b43 100644
--- a/packages/frontend/editor-ui/package.json
+++ b/packages/frontend/editor-ui/package.json
@@ -59,6 +59,7 @@
     "core-js": "^3.40.0",
     "curlconverter": "^4.12.0",
     "dateformat": "^3.0.3",
+    "element-plus": "catalog:frontend",
     "email-providers": "^2.0.1",
     "esprima-next": "5.8.4",
     "fast-json-stable-stringify": "^2.1.0",
diff --git a/packages/frontend/editor-ui/src/Interface.ts b/packages/frontend/editor-ui/src/Interface.ts
index c44867141dcd3..2016b7db0aa11 100644
--- a/packages/frontend/editor-ui/src/Interface.ts
+++ b/packages/frontend/editor-ui/src/Interface.ts
@@ -445,9 +445,9 @@ export interface IExecutionBase {
 	status: ExecutionStatus;
 	retryOf?: string;
 	retrySuccessId?: string;
-	startedAt: Date;
-	createdAt: Date;
-	stoppedAt?: Date;
+	startedAt: Date | string;
+	createdAt: Date | string;
+	stoppedAt?: Date | string;
 	workflowId?: string; // To be able to filter executions easily //
 }
 
@@ -1583,3 +1583,10 @@ export type MainPanelDimensions = Record<
 		relativeWidth: number;
 	}
 >;
+
+export interface LlmTokenUsageData {
+	completionTokens: number;
+	promptTokens: number;
+	totalTokens: number;
+	isEstimate: boolean;
+}
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue
index eea3e96b97f5b..f4a70d35989b6 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue
@@ -10,7 +10,7 @@ import ChatInput from '@n8n/chat/components/Input.vue';
 import { computed, ref } from 'vue';
 import { useClipboard } from '@/composables/useClipboard';
 import { useToast } from '@/composables/useToast';
-import PanelHeader from '@/components/CanvasChat/components/PanelHeader.vue';
+import PanelHeader from '@/components/CanvasChat/future/components/PanelHeader.vue';
 import { N8nButton, N8nIconButton, N8nTooltip } from '@n8n/design-system';
 
 interface Props {
@@ -357,7 +357,7 @@ async function copySessionId() {
 	height: 100%;
 	width: 100%;
 	overflow: auto;
-	padding-top: 1.5em;
+	padding-top: var(--spacing-l);
 
 	&:not(:last-child) {
 		margin-right: 1em;
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue
index 396e578d35b31..d6ec7c6c1d98e 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue
@@ -32,10 +32,8 @@ const telemetry = useTelemetry();
 const { rootStyles, height, chatWidth, onWindowResize, onResizeDebounced, onResizeChatDebounced } =
 	useResize(container);
 
-const { currentSessionId, messages, sendMessage, refreshSession, displayExecution } = useChatState(
-	ref(false),
-	onWindowResize,
-);
+const { currentSessionId, messages, connectedNode, sendMessage, refreshSession, displayExecution } =
+	useChatState(ref(false), onWindowResize);
 const appStyles = useStyles();
 const tooltipZIndex = computed(() => appStyles.APP_Z_INDEXES.ASK_ASSISTANT_FLOATING_BUTTON + 100);
 
@@ -95,7 +93,7 @@ watch([panelState, height], ([state, h]) => {
 				:class="[$style.resizeWrapper, panelState === 'closed' ? '' : $style.isOpen]"
 				@resize="onResizeDebounced"
 			>
-				<div ref="container" :class="$style.container">
+				<div ref="container" :class="[$style.container, 'ignore-key-press-canvas']" tabindex="0">
 					<N8nResizeWrapper
 						v-if="hasChat"
 						:supported-directions="['right']"
@@ -120,7 +118,11 @@ watch([panelState, height], ([state, h]) => {
 							@click-header="handleClickHeader"
 						/>
 					</N8nResizeWrapper>
-					<LogsOverviewPanel :is-open="panelState !== 'closed'" @click-header="handleClickHeader">
+					<LogsOverviewPanel
+						:is-open="panelState !== 'closed'"
+						:node="connectedNode"
+						@click-header="handleClickHeader"
+					>
 						<template #actions>
 							<N8nTooltip
 								v-if="canPopOut && !isPoppedOut"
@@ -178,14 +180,11 @@ watch([panelState, height], ([state, h]) => {
 }
 
 .resizeWrapper {
-	height: auto;
+	height: 100%;
 	min-height: 0;
 	flex-basis: 0;
-	border-top: 1px solid var(--color-foreground-base);
+	border-top: var(--border-base);
 	background-color: var(--color-background-light);
-	display: flex;
-	align-items: stretch;
-	justify-content: stretch;
 
 	&.isOpen {
 		height: var(--panel-height);
@@ -196,11 +195,12 @@ watch([panelState, height], ([state, h]) => {
 }
 
 .container {
+	height: 100%;
 	display: flex;
 	flex-grow: 1;
 
 	& > *:not(:last-child) {
-		border-right: 1px solid var(--color-foreground-base);
+		border-right: var(--border-base);
 	}
 }
 
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/ConsumedTokenCountText.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/ConsumedTokenCountText.vue
new file mode 100644
index 0000000000000..7e9155126cfdb
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/ConsumedTokenCountText.vue
@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import { formatTokenUsageCount } from '@/components/RunDataAi/utils';
+import { useI18n } from '@/composables/useI18n';
+import { type LlmTokenUsageData } from '@/Interface';
+import { N8nTooltip } from '@n8n/design-system';
+
+const { consumedTokens } = defineProps<{ consumedTokens: LlmTokenUsageData }>();
+const locale = useI18n();
+</script>
+
+<template>
+	<N8nTooltip v-if="consumedTokens !== undefined" :enterable="false">
+		<span>{{
+			locale.baseText('runData.aiContentBlock.tokens', {
+				interpolate: {
+					count: formatTokenUsageCount(consumedTokens, 'total'),
+				},
+			})
+		}}</span>
+		<template #content>
+			<ConsumedTokensDetails :consumed-tokens="consumedTokens" />
+		</template>
+	</N8nTooltip>
+</template>
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts
new file mode 100644
index 0000000000000..ddf5e9ecfefa3
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts
@@ -0,0 +1,148 @@
+import { renderComponent } from '@/__tests__/render';
+import LogsOverviewPanel from './LogsOverviewPanel.vue';
+import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
+import { setActivePinia } from 'pinia';
+import { createTestingPinia, type TestingPinia } from '@pinia/testing';
+import { mockedStore } from '@/__tests__/utils';
+import { useWorkflowsStore } from '@/stores/workflows.store';
+import { createRouter, createWebHistory } from 'vue-router';
+import { h, type ExtractPropTypes } from 'vue';
+import { fireEvent, waitFor, within } from '@testing-library/vue';
+
+describe('LogsOverviewPanel', () => {
+	let pinia: TestingPinia;
+	let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
+
+	const triggerNode = createTestNode({ name: 'Chat' });
+	const aiAgentNode = createTestNode({ name: 'AI Agent' });
+	const aiModelNode = createTestNode({ name: 'AI Model' });
+	const workflow = createTestWorkflow({
+		nodes: [triggerNode, aiAgentNode, aiModelNode],
+		connections: {
+			Chat: {
+				main: [[{ node: 'AI Agent', index: 0, type: 'main' }]],
+			},
+			'AI Model': {
+				ai_languageModel: [[{ node: 'AI Agent', index: 0, type: 'ai_languageModel' }]],
+			},
+		},
+	});
+
+	function render(props: ExtractPropTypes<typeof LogsOverviewPanel>) {
+		return renderComponent(LogsOverviewPanel, {
+			props,
+			global: {
+				plugins: [
+					createRouter({
+						history: createWebHistory(),
+						routes: [{ path: '/', component: () => h('div') }],
+					}),
+					pinia,
+				],
+			},
+		});
+	}
+
+	beforeEach(() => {
+		pinia = createTestingPinia({ stubActions: false, fakeApp: true });
+
+		setActivePinia(pinia);
+
+		workflowsStore = mockedStore(useWorkflowsStore);
+		workflowsStore.setWorkflow(workflow);
+		workflowsStore.setWorkflowExecutionData(null);
+	});
+
+	it('should not render body if the panel is not open', () => {
+		const rendered = render({ isOpen: false, node: null });
+
+		expect(
+			rendered.queryByText('Nothing to display yet', { exact: false }),
+		).not.toBeInTheDocument();
+	});
+
+	it('should render empty text if there is no execution', () => {
+		const rendered = render({ isOpen: true, node: null });
+
+		expect(rendered.queryByText('Nothing to display yet', { exact: false })).toBeInTheDocument();
+	});
+
+	it('should render summary text and executed nodes if there is an execution', async () => {
+		workflowsStore.setWorkflowExecutionData({
+			id: 'test-exec-id',
+			finished: true,
+			mode: 'manual',
+			status: 'success',
+			data: {
+				resultData: {
+					runData: {
+						'AI Agent': [
+							{
+								executionStatus: 'success',
+								startTime: +new Date('2025-03-26T00:00:00.002Z'),
+								executionTime: 1778,
+								source: [],
+								data: {},
+							},
+						],
+						'AI Model': [
+							{
+								executionStatus: 'success',
+								startTime: +new Date('2025-03-26T00:00:00.003Z'),
+								executionTime: 1777,
+								source: [],
+								data: {
+									ai_languageModel: [
+										[
+											{
+												json: {
+													tokenUsage: {
+														completionTokens: 222,
+														promptTokens: 333,
+														totalTokens: 555,
+													},
+												},
+											},
+										],
+									],
+								},
+							},
+						],
+					},
+				},
+			},
+			workflowData: workflow,
+			createdAt: new Date('2025-03-26T00:00:00.000Z'),
+			startedAt: new Date('2025-03-26T00:00:00.001Z'),
+			stoppedAt: new Date('2025-03-26T00:00:02.000Z'),
+		});
+
+		const rendered = render({ isOpen: true, node: aiAgentNode });
+		const summary = within(rendered.container.querySelector('.summary')!);
+
+		expect(summary.queryByText('Success in 1.999s')).toBeInTheDocument();
+		expect(summary.queryByText('555 Tokens')).toBeInTheDocument();
+
+		const tree = within(rendered.getByRole('tree'));
+
+		expect(tree.queryAllByRole('treeitem')).toHaveLength(2);
+
+		const row1 = within(tree.queryAllByRole('treeitem')[0]);
+
+		expect(row1.queryByText('AI Agent')).toBeInTheDocument();
+		expect(row1.queryByText('Success in 1.778s')).toBeInTheDocument();
+		expect(row1.queryByText('Started 2025-03-26T00:00:00.002Z')).toBeInTheDocument();
+		expect(row1.queryByText('555 Tokens')).toBeInTheDocument();
+
+		const row2 = within(tree.queryAllByRole('treeitem')[1]);
+
+		expect(row2.queryByText('AI Model')).toBeInTheDocument();
+		expect(row2.queryByText('Success in 1.777s')).toBeInTheDocument();
+		expect(row2.queryByText('Started 2025-03-26T00:00:00.003Z')).toBeInTheDocument();
+		expect(row2.queryByText('555 Tokens')).toBeInTheDocument();
+
+		// collapse tree
+		await fireEvent.click(row1.getByRole('button'));
+		await waitFor(() => expect(tree.queryAllByRole('treeitem')).toHaveLength(1));
+	});
+});
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue
index e6a8a8a67613e..b6a4d15baa74d 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue
@@ -1,25 +1,100 @@
 <script setup lang="ts">
-import PanelHeader from '@/components/CanvasChat/components/PanelHeader.vue';
+import PanelHeader from '@/components/CanvasChat/future/components/PanelHeader.vue';
 import { useClearExecutionButtonVisible } from '@/composables/useClearExecutionButtonVisible';
 import { useI18n } from '@/composables/useI18n';
 import { useNodeHelpers } from '@/composables/useNodeHelpers';
 import { useWorkflowsStore } from '@/stores/workflows.store';
-import { N8nButton, N8nText, N8nTooltip } from '@n8n/design-system';
+import { N8nButton, N8nRadioButtons, N8nText, N8nTooltip } from '@n8n/design-system';
+import { computed, ref } from 'vue';
+import { ElTree, type TreeNode as ElTreeNode } from 'element-plus';
+import {
+	createAiData,
+	getSubtreeTotalConsumedTokens,
+	getTotalConsumedTokens,
+	getTreeNodeData,
+	type TreeNode,
+} from '@/components/RunDataAi/utils';
+import { type INodeUi } from '@/Interface';
+import { upperFirst } from 'lodash-es';
+import { useTelemetry } from '@/composables/useTelemetry';
+import ConsumedTokenCountText from '@/components/CanvasChat/future/components/ConsumedTokenCountText.vue';
 
-defineProps<{ isOpen: boolean }>();
+const { node, isOpen } = defineProps<{ isOpen: boolean; node: INodeUi | null }>();
 
 const emit = defineEmits<{ clickHeader: [] }>();
+
+defineSlots<{ actions: {} }>();
+
 const locale = useI18n();
+const telemetry = useTelemetry();
 const workflowsStore = useWorkflowsStore();
 const nodeHelpers = useNodeHelpers();
 const isClearExecutionButtonVisible = useClearExecutionButtonVisible();
+const workflow = computed(() => workflowsStore.getCurrentWorkflow());
+const executionTree = computed<TreeNode[]>(() =>
+	node
+		? getTreeNodeData(
+				node.name,
+				workflow.value,
+				createAiData(node.name, workflow.value, workflowsStore.getWorkflowResultDataByNodeName),
+			)
+		: [],
+);
+const isEmpty = computed(() => workflowsStore.workflowExecutionData === null);
+const switchViewOptions = computed<Array<{ label: string; value: string }>>(() => [
+	{ label: locale.baseText('logs.overview.header.switch.details'), value: 'details' },
+	{ label: locale.baseText('logs.overview.header.switch.overview'), value: 'overview' },
+]);
+const executionStatusText = computed(() => {
+	const execution = workflowsStore.workflowExecutionData;
 
-defineSlots<{ actions: {} }>();
+	if (!execution) {
+		return undefined;
+	}
+
+	if (execution.startedAt && execution.stoppedAt) {
+		return locale.baseText('logs.overview.body.summaryText', {
+			interpolate: {
+				status: upperFirst(execution.status),
+				time: locale.displayTimer(
+					+new Date(execution.stoppedAt) - +new Date(execution.startedAt),
+					true,
+				),
+			},
+		});
+	}
+
+	return upperFirst(execution.status);
+});
+const consumedTokens = computed(() =>
+	getTotalConsumedTokens(...executionTree.value.map(getSubtreeTotalConsumedTokens)),
+);
+
+const selectedRun = ref<{ node: string; runIndex: number } | undefined>(undefined);
 
 function onClearExecutionData() {
 	workflowsStore.setWorkflowExecutionData(null);
 	nodeHelpers.updateNodesExecutionIssues();
 }
+
+function handleClickNode(clicked: TreeNode) {
+	if (selectedRun.value?.node === clicked.node && selectedRun.value.runIndex === clicked.runIndex) {
+		selectedRun.value = undefined;
+		return;
+	}
+
+	selectedRun.value = { node: clicked.node, runIndex: clicked.runIndex };
+	telemetry.track('User selected node in log view', {
+		node_type: workflowsStore.nodesByName[clicked.node].type,
+		node_id: workflowsStore.nodesByName[clicked.node].id,
+		execution_id: workflowsStore.workflowExecutionData?.id,
+		workflow_id: workflow.value.id,
+	});
+}
+
+function handleToggleExpanded(treeNode: ElTreeNode) {
+	treeNode.expanded = !treeNode.expanded;
+}
 </script>
 
 <template>
@@ -45,10 +120,52 @@ function onClearExecutionData() {
 				<slot name="actions" />
 			</template>
 		</PanelHeader>
-		<div v-if="isOpen" :class="[$style.content, $style.empty]">
-			<N8nText tag="p" size="medium" color="text-base" :class="$style.emptyText">
+		<div v-if="isOpen" :class="[$style.content, isEmpty ? $style.empty : '']">
+			<N8nText v-if="isEmpty" tag="p" size="medium" color="text-base" :class="$style.emptyText">
 				{{ locale.baseText('logs.overview.body.empty.message') }}
 			</N8nText>
+			<div v-else :class="$style.scrollable">
+				<N8nText
+					v-if="executionStatusText !== undefined"
+					tag="div"
+					color="text-light"
+					size="small"
+					:class="$style.summary"
+				>
+					<span>{{ executionStatusText }}</span>
+					<ConsumedTokenCountText
+						v-if="consumedTokens.totalTokens > 0"
+						:consumed-tokens="consumedTokens"
+					/>
+				</N8nText>
+				<ElTree
+					v-if="executionTree.length > 0"
+					:class="$style.tree"
+					:indent="0"
+					:data="executionTree"
+					:expand-on-click-node="false"
+					:default-expand-all="true"
+					@node-click="handleClickNode"
+				>
+					<template #default="{ node: elTreeNode, data }">
+						<LogsOverviewRow
+							:data="data"
+							:node="elTreeNode"
+							:is-selected="
+								data.node === selectedRun?.node && data.runIndex === selectedRun?.runIndex
+							"
+							:should-show-consumed-tokens="consumedTokens.totalTokens > 0"
+							@toggle-expanded="handleToggleExpanded"
+						/>
+					</template>
+				</ElTree>
+				<N8nRadioButtons
+					size="medium"
+					:class="$style.switchViewButtons"
+					:model-value="selectedRun ? 'details' : 'overview'"
+					:options="switchViewOptions"
+				/>
+			</div>
 		</div>
 	</div>
 </template>
@@ -60,14 +177,19 @@ function onClearExecutionData() {
 	display: flex;
 	flex-direction: column;
 	align-items: stretch;
+	overflow: hidden;
 }
 
 .content {
-	padding: var(--spacing-2xs);
+	position: relative;
 	flex-grow: 1;
+	overflow: auto;
+	display: flex;
+	flex-direction: column;
+	align-items: stretch;
+	justify-content: stretch;
 
 	&.empty {
-		display: flex;
 		align-items: center;
 		justify-content: center;
 	}
@@ -77,4 +199,40 @@ function onClearExecutionData() {
 	max-width: 20em;
 	text-align: center;
 }
+
+.scrollable {
+	padding: var(--spacing-2xs);
+	flex-grow: 1;
+	flex-shrink: 1;
+	overflow: auto;
+}
+
+.summary {
+	display: flex;
+	align-items: center;
+	padding-block: var(--spacing-2xs);
+
+	& > * {
+		padding-inline: var(--spacing-2xs);
+	}
+
+	& > *:not(:last-child) {
+		border-right: var(--border-base);
+	}
+}
+
+.tree {
+	margin-top: var(--spacing-2xs);
+
+	& :global(.el-icon) {
+		display: none;
+	}
+}
+
+.switchViewButtons {
+	position: absolute;
+	right: 0;
+	top: 0;
+	margin: var(--spacing-2xs);
+}
 </style>
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue
new file mode 100644
index 0000000000000..edf3006eddb95
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue
@@ -0,0 +1,239 @@
+<script setup lang="ts">
+import { type TreeNode as ElTreeNode } from 'element-plus';
+import { getSubtreeTotalConsumedTokens, type TreeNode } from '@/components/RunDataAi/utils';
+import { useWorkflowsStore } from '@/stores/workflows.store';
+import { computed } from 'vue';
+import { type INodeUi } from '@/Interface';
+import { type ExecutionStatus, type ITaskData } from 'n8n-workflow';
+import { N8nIconButton, N8nText } from '@n8n/design-system';
+import { useNodeTypesStore } from '@/stores/nodeTypes.store';
+import { upperFirst } from 'lodash-es';
+import { useI18n } from '@/composables/useI18n';
+import ConsumedTokenCountText from '@/components/CanvasChat/future/components/ConsumedTokenCountText.vue';
+
+const props = defineProps<{
+	data: TreeNode;
+	node: ElTreeNode;
+	isSelected: boolean;
+	shouldShowConsumedTokens: boolean;
+}>();
+
+const emit = defineEmits<{ toggleExpanded: [node: ElTreeNode] }>();
+
+const locale = useI18n();
+const workflowsStore = useWorkflowsStore();
+const nodeTypeStore = useNodeTypesStore();
+const node = computed<INodeUi | undefined>(() => workflowsStore.nodesByName[props.data.node]);
+const runData = computed<ITaskData | undefined>(() =>
+	node.value
+		? workflowsStore.workflowExecutionData?.data?.resultData.runData[node.value.name]?.[
+				props.data.runIndex
+			]
+		: undefined,
+);
+const type = computed(() => (node.value ? nodeTypeStore.getNodeType(node.value.type) : undefined));
+const depth = computed(() => (props.node.level ?? 1) - 1);
+const timeTookText = computed(() => {
+	const finalStatuses: ExecutionStatus[] = ['crashed', 'error', 'success'];
+	const status = runData.value?.executionStatus;
+
+	if (!status) {
+		return '';
+	}
+
+	const statusText = upperFirst(status);
+
+	return finalStatuses.includes(status)
+		? locale.baseText('logs.overview.body.summaryText', {
+				interpolate: {
+					status: statusText,
+					time: locale.displayTimer(runData.value.executionTime, true),
+				},
+			})
+		: statusText;
+});
+const startedAtText = computed(() =>
+	locale.baseText('logs.overview.body.started', {
+		interpolate: {
+			time: new Date(runData.value?.startTime ?? 0).toISOString(), // TODO: confirm date format
+		},
+	}),
+);
+
+const subtreeConsumedTokens = computed(() =>
+	props.shouldShowConsumedTokens ? getSubtreeTotalConsumedTokens(props.data) : undefined,
+);
+
+function isLastChild(level: number) {
+	let parent = props.data.parent;
+	let data: TreeNode | undefined = props.data;
+
+	for (let i = 0; i < depth.value - level; i++) {
+		data = parent;
+		parent = parent?.parent;
+	}
+
+	const siblings = parent?.children ?? [];
+
+	return data === siblings[siblings.length - 1];
+}
+</script>
+
+<template>
+	<div
+		v-if="node !== undefined"
+		:class="{ [$style.container]: true, [$style.selected]: props.isSelected }"
+	>
+		<template v-for="level in depth" :key="level">
+			<div
+				:class="{
+					[$style.indent]: true,
+					[$style.connectorCurved]: level === depth,
+					[$style.connectorStraight]: !isLastChild(level),
+				}"
+			/>
+		</template>
+		<NodeIcon :node-type="type" :size="16" :class="$style.icon" />
+		<N8nText tag="div" :bold="true" size="small" :class="$style.name">{{ node.name }}</N8nText>
+		<N8nText tag="div" color="text-light" size="small" :class="$style.timeTook">{{
+			timeTookText
+		}}</N8nText>
+		<N8nText tag="div" color="text-light" size="small" :class="$style.startedAt">{{
+			startedAtText
+		}}</N8nText>
+		<N8nText
+			v-if="subtreeConsumedTokens !== undefined"
+			tag="div"
+			color="text-light"
+			size="small"
+			:class="$style.consumedTokens"
+		>
+			<ConsumedTokenCountText
+				v-if="
+					subtreeConsumedTokens.totalTokens > 0 &&
+					(props.data.children.length === 0 || !props.node.expanded)
+				"
+				:consumed-tokens="subtreeConsumedTokens"
+			/>
+		</N8nText>
+		<div>
+			<N8nIconButton
+				type="secondary"
+				size="medium"
+				:icon="props.node.expanded ? 'chevron-down' : 'chevron-up'"
+				:style="{
+					visibility: props.data.children.length === 0 ? 'hidden' : '',
+					color: 'var(--color-text-base)', // give higher specificity than the style from the component itself
+				}"
+				:class="$style.toggleButton"
+				@click.stop="emit('toggleExpanded', props.node)"
+			/>
+		</div>
+	</div>
+</template>
+
+<style lang="scss" module>
+.container {
+	display: flex;
+	align-items: stretch;
+	justify-content: stretch;
+	overflow: hidden;
+
+	& > * {
+		overflow: hidden;
+		text-overflow: ellipsis;
+		white-space: nowrap;
+		padding: var(--spacing-2xs);
+	}
+
+	& > :has(.toggleButton) {
+		flex-shrink: 0;
+		display: flex;
+		align-items: center;
+		padding: 0;
+	}
+
+	& > .icon {
+		border-top-left-radius: var(--border-radius-base);
+		border-bottom-left-radius: var(--border-radius-base);
+	}
+
+	& > :last-of-type {
+		border-top-right-radius: var(--border-radius-base);
+		border-bottom-right-radius: var(--border-radius-base);
+	}
+
+	&.selected > :not(.indent),
+	&:hover > :not(.indent) {
+		background-color: var(--color-foreground-base);
+	}
+}
+
+.indent {
+	flex-grow: 0;
+	flex-shrink: 0;
+	width: var(--spacing-xl);
+	align-self: stretch;
+	position: relative;
+	overflow: hidden;
+
+	&.connectorCurved:before {
+		content: '';
+		position: absolute;
+		left: var(--spacing-s);
+		bottom: var(--spacing-s);
+		border: 2px solid var(--color-canvas-dot);
+		width: var(--spacing-l);
+		height: var(--spacing-l);
+		border-radius: var(--border-radius-large);
+	}
+
+	&.connectorStraight:after {
+		content: '';
+		position: absolute;
+		left: var(--spacing-s);
+		top: 0;
+		border-left: 2px solid var(--color-canvas-dot);
+		height: 100%;
+	}
+}
+
+.icon {
+	flex-grow: 0;
+	flex-shrink: 0;
+}
+
+.name {
+	flex-grow: 1;
+	padding-inline-start: 0;
+}
+
+.timeTook {
+	flex-grow: 0;
+	flex-shrink: 0;
+	width: 20%;
+}
+
+.startedAt {
+	flex-grow: 0;
+	flex-shrink: 0;
+	width: 30%;
+}
+
+.consumedTokens {
+	flex-grow: 0;
+	flex-shrink: 0;
+	width: 10%;
+	text-align: right;
+}
+
+.toggleButton {
+	border: none;
+	background: transparent;
+	margin-inline-end: var(--spacing-5xs);
+
+	&:hover {
+		background: transparent;
+	}
+}
+</style>
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/components/PanelHeader.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/PanelHeader.vue
similarity index 80%
rename from packages/frontend/editor-ui/src/components/CanvasChat/components/PanelHeader.vue
rename to packages/frontend/editor-ui/src/components/CanvasChat/future/components/PanelHeader.vue
index e5c77edf4f74e..fbad423a2f28e 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/components/PanelHeader.vue
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/PanelHeader.vue
@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import { N8nText } from '@n8n/design-system';
+
 defineProps<{ title: string }>();
 
 defineSlots<{ actions: {} }>();
@@ -8,7 +10,7 @@ const emit = defineEmits<{ click: [] }>();
 
 <template>
 	<header :class="$style.container" @click="emit('click')">
-		<span :class="$style.title">{{ title }}</span>
+		<N8nText :class="$style.title" :bold="true" size="small">{{ title }}</N8nText>
 		<div :class="$style.actions">
 			<slot name="actions" />
 		</div>
@@ -18,10 +20,9 @@ const emit = defineEmits<{ click: [] }>();
 <style lang="scss" module>
 .container {
 	font-size: var(--font-size-2xs);
-	font-weight: 400;
-	line-height: 18px;
 	text-align: left;
-	padding-inline: var(--spacing-s);
+	padding-inline-start: var(--spacing-s);
+	padding-inline-end: var(--spacing-xs);
 	padding-block: var(--spacing-2xs);
 	background-color: var(--color-foreground-xlight);
 	display: flex;
@@ -36,12 +37,11 @@ const emit = defineEmits<{ click: [] }>();
 
 	&:not(:last-child) {
 		/** Panel open */
-		border-bottom: 1px solid var(--color-foreground-base);
+		border-bottom: var(--border-base);
 	}
 }
 
 .title {
-	font-weight: 600;
 	flex-grow: 1;
 	flex-shrink: 1;
 }
diff --git a/packages/frontend/editor-ui/src/components/ConsumedTokensDetails.vue b/packages/frontend/editor-ui/src/components/ConsumedTokensDetails.vue
new file mode 100644
index 0000000000000..a708ba0237c06
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/ConsumedTokensDetails.vue
@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import { formatTokenUsageCount } from '@/components/RunDataAi/utils';
+import { useI18n } from '@/composables/useI18n';
+import { type LlmTokenUsageData } from '@/Interface';
+import { N8nText } from '@n8n/design-system';
+
+const { consumedTokens } = defineProps<{ consumedTokens: LlmTokenUsageData }>();
+const i18n = useI18n();
+</script>
+
+<template>
+	<div>
+		<N8nText :bold="true" size="small">
+			{{ i18n.baseText('runData.aiContentBlock.tokens.prompt') }}
+			{{
+				i18n.baseText('runData.aiContentBlock.tokens', {
+					interpolate: {
+						count: formatTokenUsageCount(consumedTokens, 'prompt'),
+					},
+				})
+			}}
+		</N8nText>
+		<br />
+		<N8nText :bold="true" size="small">
+			{{ i18n.baseText('runData.aiContentBlock.tokens.completion') }}
+			{{
+				i18n.baseText('runData.aiContentBlock.tokens', {
+					interpolate: {
+						count: formatTokenUsageCount(consumedTokens, 'completion'),
+					},
+				})
+			}}
+		</N8nText>
+	</div>
+</template>
diff --git a/packages/frontend/editor-ui/src/components/RunDataAi/RunDataAiContent.vue b/packages/frontend/editor-ui/src/components/RunDataAi/RunDataAiContent.vue
index adacd5f30e290..cd6cd416cf46d 100644
--- a/packages/frontend/editor-ui/src/components/RunDataAi/RunDataAiContent.vue
+++ b/packages/frontend/editor-ui/src/components/RunDataAi/RunDataAiContent.vue
@@ -2,17 +2,14 @@
 import type { IAiData, IAiDataContent } from '@/Interface';
 import { useNodeTypesStore } from '@/stores/nodeTypes.store';
 import { useWorkflowsStore } from '@/stores/workflows.store';
-import type {
-	INodeExecutionData,
-	INodeTypeDescription,
-	NodeConnectionType,
-	NodeError,
-} from 'n8n-workflow';
+import type { INodeTypeDescription, NodeConnectionType, NodeError } from 'n8n-workflow';
 import { computed } from 'vue';
 import NodeIcon from '@/components/NodeIcon.vue';
 import AiRunContentBlock from './AiRunContentBlock.vue';
 import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
 import { useI18n } from '@/composables/useI18n';
+import { formatTokenUsageCount, getConsumedTokens } from '@/components/RunDataAi/utils';
+import ConsumedTokensDetails from '@/components/ConsumedTokensDetails.vue';
 
 interface RunMeta {
 	startTimeMs: number;
@@ -36,44 +33,11 @@ const workflowsStore = useWorkflowsStore();
 const { trackOpeningRelatedExecution, resolveRelatedExecutionUrl } = useExecutionHelpers();
 const i18n = useI18n();
 
-type TokenUsageData = {
-	completionTokens: number;
-	promptTokens: number;
-	totalTokens: number;
-};
-
 const consumedTokensSum = computed(() => {
 	// eslint-disable-next-line @typescript-eslint/no-use-before-define
-	const tokenUsage = outputRun.value?.data?.reduce(
-		(acc: TokenUsageData, curr: INodeExecutionData) => {
-			const tokenUsageData = (curr.json?.tokenUsage ??
-				curr.json?.tokenUsageEstimate) as TokenUsageData;
-
-			if (!tokenUsageData) return acc;
-
-			return {
-				completionTokens: acc.completionTokens + tokenUsageData.completionTokens,
-				promptTokens: acc.promptTokens + tokenUsageData.promptTokens,
-				totalTokens: acc.totalTokens + tokenUsageData.totalTokens,
-			};
-		},
-		{
-			completionTokens: 0,
-			promptTokens: 0,
-			totalTokens: 0,
-		},
-	);
-
-	return tokenUsage;
-});
-
-const usingTokensEstimates = computed(() => {
-	return outputRun.value?.data?.some((d) => d.json?.tokenUsageEstimate);
+	return getConsumedTokens(outputRun.value);
 });
 
-function formatTokenUsageCount(count: number) {
-	return usingTokensEstimates.value ? `~${count}` : count.toString();
-}
 function extractRunMeta(run: IAiDataContent) {
 	const uiNode = workflowsStore.getNodeByName(props.inputData.node);
 	const nodeType = nodeTypesStore.getNodeType(uiNode?.type ?? '');
@@ -155,34 +119,12 @@ const outputError = computed(() => {
 						{{
 							i18n.baseText('runData.aiContentBlock.tokens', {
 								interpolate: {
-									count: formatTokenUsageCount(consumedTokensSum?.totalTokens ?? 0),
+									count: formatTokenUsageCount(consumedTokensSum, 'total'),
 								},
 							})
 						}}
 						<n8n-info-tip type="tooltip" theme="info-light" tooltip-placement="right">
-							<div>
-								<n8n-text :bold="true" size="small">
-									{{ i18n.baseText('runData.aiContentBlock.tokens.prompt') }}
-									{{
-										i18n.baseText('runData.aiContentBlock.tokens', {
-											interpolate: {
-												count: formatTokenUsageCount(consumedTokensSum?.promptTokens ?? 0),
-											},
-										})
-									}}
-								</n8n-text>
-								<br />
-								<n8n-text :bold="true" size="small">
-									{{ i18n.baseText('runData.aiContentBlock.tokens.completion') }}
-									{{
-										i18n.baseText('runData.aiContentBlock.tokens', {
-											interpolate: {
-												count: formatTokenUsageCount(consumedTokensSum?.completionTokens ?? 0),
-											},
-										})
-									}}
-								</n8n-text>
-							</div>
+							<ConsumedTokensDetails :consumed-tokens="consumedTokensSum" />
 						</n8n-info-tip>
 					</li>
 				</ul>
diff --git a/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts b/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts
index 9140c853e4422..a85f801d0f9e2 100644
--- a/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts
+++ b/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts
@@ -31,11 +31,62 @@ describe(getTreeNodeData, () => {
 		const taskDataByNodeName: Record<string, ITaskData[]> = {
 			A: [createTaskData({ startTime: +new Date('2025-02-26T00:00:00.000Z') })],
 			B: [
-				createTaskData({ startTime: +new Date('2025-02-26T00:00:01.000Z') }),
-				createTaskData({ startTime: +new Date('2025-02-26T00:00:03.000Z') }),
+				createTaskData({
+					startTime: +new Date('2025-02-26T00:00:01.000Z'),
+					data: {
+						main: [
+							[
+								{
+									json: {
+										tokenUsage: {
+											completionTokens: 1,
+											promptTokens: 2,
+											totalTokens: 3,
+										},
+									},
+								},
+							],
+						],
+					},
+				}),
+				createTaskData({
+					startTime: +new Date('2025-02-26T00:00:03.000Z'),
+					data: {
+						main: [
+							[
+								{
+									json: {
+										tokenUsage: {
+											completionTokens: 4,
+											promptTokens: 5,
+											totalTokens: 6,
+										},
+									},
+								},
+							],
+						],
+					},
+				}),
 			],
 			C: [
-				createTaskData({ startTime: +new Date('2025-02-26T00:00:02.000Z') }),
+				createTaskData({
+					startTime: +new Date('2025-02-26T00:00:02.000Z'),
+					data: {
+						main: [
+							[
+								{
+									json: {
+										tokenUsageEstimate: {
+											completionTokens: 7,
+											promptTokens: 8,
+											totalTokens: 9,
+										},
+									},
+								},
+							],
+						],
+					},
+				}),
 				createTaskData({ startTime: +new Date('2025-02-26T00:00:04.000Z') }),
 			],
 		};
@@ -53,6 +104,13 @@ describe(getTreeNodeData, () => {
 				node: 'A',
 				runIndex: 0,
 				startTime: 0,
+				parent: undefined,
+				consumedTokens: {
+					completionTokens: 0,
+					promptTokens: 0,
+					totalTokens: 0,
+					isEstimate: false,
+				},
 				children: [
 					{
 						depth: 1,
@@ -60,6 +118,13 @@ describe(getTreeNodeData, () => {
 						node: 'B',
 						runIndex: 0,
 						startTime: +new Date('2025-02-26T00:00:01.000Z'),
+						parent: expect.objectContaining({ node: 'A' }),
+						consumedTokens: {
+							completionTokens: 1,
+							promptTokens: 2,
+							totalTokens: 3,
+							isEstimate: false,
+						},
 						children: [
 							{
 								children: [],
@@ -68,6 +133,13 @@ describe(getTreeNodeData, () => {
 								node: 'C',
 								runIndex: 0,
 								startTime: +new Date('2025-02-26T00:00:02.000Z'),
+								parent: expect.objectContaining({ node: 'B' }),
+								consumedTokens: {
+									completionTokens: 7,
+									promptTokens: 8,
+									totalTokens: 9,
+									isEstimate: true,
+								},
 							},
 						],
 					},
@@ -77,6 +149,13 @@ describe(getTreeNodeData, () => {
 						node: 'B',
 						runIndex: 1,
 						startTime: +new Date('2025-02-26T00:00:03.000Z'),
+						parent: expect.objectContaining({ node: 'A' }),
+						consumedTokens: {
+							completionTokens: 4,
+							promptTokens: 5,
+							totalTokens: 6,
+							isEstimate: false,
+						},
 						children: [
 							{
 								children: [],
@@ -85,6 +164,13 @@ describe(getTreeNodeData, () => {
 								node: 'C',
 								runIndex: 1,
 								startTime: +new Date('2025-02-26T00:00:04.000Z'),
+								parent: expect.objectContaining({ node: 'B' }),
+								consumedTokens: {
+									completionTokens: 0,
+									promptTokens: 0,
+									totalTokens: 0,
+									isEstimate: false,
+								},
 							},
 						],
 					},
diff --git a/packages/frontend/editor-ui/src/components/RunDataAi/utils.ts b/packages/frontend/editor-ui/src/components/RunDataAi/utils.ts
index 9c8dbb43dbc44..ba080dfc51b02 100644
--- a/packages/frontend/editor-ui/src/components/RunDataAi/utils.ts
+++ b/packages/frontend/editor-ui/src/components/RunDataAi/utils.ts
@@ -1,5 +1,6 @@
-import { type IAiDataContent } from '@/Interface';
+import { type LlmTokenUsageData, type IAiDataContent } from '@/Interface';
 import {
+	type INodeExecutionData,
 	type ITaskData,
 	type ITaskDataConnections,
 	type NodeConnectionType,
@@ -13,27 +14,32 @@ export interface AIResult {
 }
 
 export interface TreeNode {
+	parent?: TreeNode;
 	node: string;
 	id: string;
 	children: TreeNode[];
 	depth: number;
 	startTime: number;
 	runIndex: number;
+	consumedTokens: LlmTokenUsageData;
 }
 
 function createNode(
+	parent: TreeNode | undefined,
 	nodeName: string,
 	currentDepth: number,
 	r?: AIResult,
 	children: TreeNode[] = [],
 ): TreeNode {
 	return {
+		parent,
 		node: nodeName,
 		id: nodeName,
 		depth: currentDepth,
 		startTime: r?.data?.metadata?.startTime ?? 0,
 		runIndex: r?.runIndex ?? 0,
 		children,
+		consumedTokens: getConsumedTokens(r?.data),
 	};
 }
 
@@ -42,10 +48,11 @@ export function getTreeNodeData(
 	workflow: Workflow,
 	aiData: AIResult[] | undefined,
 ): TreeNode[] {
-	return getTreeNodeDataRec(nodeName, 0, workflow, aiData, undefined);
+	return getTreeNodeDataRec(undefined, nodeName, 0, workflow, aiData, undefined);
 }
 
 function getTreeNodeDataRec(
+	parent: TreeNode | undefined,
 	nodeName: string,
 	currentDepth: number,
 	workflow: Workflow,
@@ -59,12 +66,13 @@ function getTreeNodeDataRec(
 		) ?? [];
 
 	if (!connections) {
-		return resultData.map((d) => createNode(nodeName, currentDepth, d));
+		return resultData.map((d) => createNode(parent, nodeName, currentDepth, d));
 	}
 
 	// Get the first level of children
 	const connectedSubNodes = workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1);
 
+	const treeNode = createNode(parent, nodeName, currentDepth);
 	const children = connectedSubNodes.flatMap((name) => {
 		// Only include sub-nodes which have data
 		return (
@@ -73,18 +81,20 @@ function getTreeNodeDataRec(
 					(data) => data.node === name && (runIndex === undefined || data.runIndex === runIndex),
 				)
 				.flatMap((data) =>
-					getTreeNodeDataRec(name, currentDepth + 1, workflow, aiData, data.runIndex),
+					getTreeNodeDataRec(treeNode, name, currentDepth + 1, workflow, aiData, data.runIndex),
 				) ?? []
 		);
 	});
 
 	children.sort((a, b) => a.startTime - b.startTime);
 
+	treeNode.children = children;
+
 	if (resultData.length) {
-		return resultData.map((r) => createNode(nodeName, currentDepth, r, children));
+		return resultData.map((r) => createNode(parent, nodeName, currentDepth, r, children));
 	}
 
-	return [createNode(nodeName, currentDepth, undefined, children)];
+	return [treeNode];
 }
 
 export function createAiData(
@@ -158,3 +168,66 @@ export function getReferencedData(
 
 	return returnData;
 }
+
+const emptyTokenUsageData: LlmTokenUsageData = {
+	completionTokens: 0,
+	promptTokens: 0,
+	totalTokens: 0,
+	isEstimate: false,
+};
+
+function addTokenUsageData(one: LlmTokenUsageData, another: LlmTokenUsageData): LlmTokenUsageData {
+	return {
+		completionTokens: one.completionTokens + another.completionTokens,
+		promptTokens: one.promptTokens + another.promptTokens,
+		totalTokens: one.totalTokens + another.totalTokens,
+		isEstimate: one.isEstimate || another.isEstimate,
+	};
+}
+
+export function getConsumedTokens(outputRun: IAiDataContent | undefined): LlmTokenUsageData {
+	if (!outputRun?.data) {
+		return emptyTokenUsageData;
+	}
+
+	const tokenUsage = outputRun.data.reduce<LlmTokenUsageData>(
+		(acc: LlmTokenUsageData, curr: INodeExecutionData) => {
+			const tokenUsageData = curr.json?.tokenUsage ?? curr.json?.tokenUsageEstimate;
+
+			if (!tokenUsageData) return acc;
+
+			return addTokenUsageData(acc, {
+				...(tokenUsageData as Omit<LlmTokenUsageData, 'isEstimate'>),
+				isEstimate: !!curr.json.tokenUsageEstimate,
+			});
+		},
+		emptyTokenUsageData,
+	);
+
+	return tokenUsage;
+}
+
+export function getTotalConsumedTokens(...usage: LlmTokenUsageData[]): LlmTokenUsageData {
+	return usage.reduce(addTokenUsageData, emptyTokenUsageData);
+}
+
+export function getSubtreeTotalConsumedTokens(treeNode: TreeNode): LlmTokenUsageData {
+	return getTotalConsumedTokens(
+		treeNode.consumedTokens,
+		...treeNode.children.map(getSubtreeTotalConsumedTokens),
+	);
+}
+
+export function formatTokenUsageCount(
+	usage: LlmTokenUsageData,
+	field: 'total' | 'prompt' | 'completion',
+) {
+	const count =
+		field === 'total'
+			? usage.totalTokens
+			: field === 'completion'
+				? usage.completionTokens
+				: usage.promptTokens;
+
+	return usage.isEstimate ? `~${count}` : count.toLocaleString();
+}
diff --git a/packages/frontend/editor-ui/src/composables/usePushConnection.ts b/packages/frontend/editor-ui/src/composables/usePushConnection.ts
index 1be7a39aac6a8..86ef569500059 100644
--- a/packages/frontend/editor-ui/src/composables/usePushConnection.ts
+++ b/packages/frontend/editor-ui/src/composables/usePushConnection.ts
@@ -240,10 +240,19 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
 
 			let showedSuccessToast = false;
 
-			let executionData: Pick<IExecutionResponse, 'workflowId' | 'data' | 'status'>;
+			let executionData: Pick<
+				IExecutionResponse,
+				'workflowId' | 'data' | 'status' | 'startedAt' | 'stoppedAt'
+			>;
 			if (receivedData.type === 'executionFinished' && receivedData.data.rawData) {
 				const { workflowId, status, rawData } = receivedData.data;
-				executionData = { workflowId, data: parse(rawData), status };
+				executionData = {
+					workflowId,
+					data: parse(rawData),
+					status,
+					startedAt: workflowsStore.workflowExecutionData?.startedAt ?? new Date(),
+					stoppedAt: new Date(),
+				};
 			} else {
 				uiStore.setProcessingExecutionResults(true);
 
@@ -278,6 +287,8 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
 						workflowId: execution.workflowId,
 						data: parse(execution.data as unknown as string),
 						status: execution.status,
+						startedAt: workflowsStore.workflowExecutionData?.startedAt as Date,
+						stoppedAt: receivedData.type === 'executionFinished' ? new Date() : undefined,
 					};
 				} catch {
 					uiStore.setProcessingExecutionResults(false);
diff --git a/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts b/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts
index 055cbbe2734a9..7baca29c22ddd 100644
--- a/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts
+++ b/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts
@@ -14,7 +14,7 @@ import {
 } from 'n8n-workflow';
 
 import { useRunWorkflow } from '@/composables/useRunWorkflow';
-import type { IStartRunData, IWorkflowData } from '@/Interface';
+import type { IExecutionResponse, IStartRunData, IWorkflowData } from '@/Interface';
 import { useWorkflowsStore } from '@/stores/workflows.store';
 import { useUIStore } from '@/stores/ui.store';
 import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
@@ -23,7 +23,8 @@ import { useI18n } from '@/composables/useI18n';
 import { captor, mock } from 'vitest-mock-extended';
 import { useSettingsStore } from '@/stores/settings.store';
 import { usePushConnectionStore } from '@/stores/pushConnection.store';
-import { createTestNode } from '@/__tests__/mocks';
+import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
+import { waitFor } from '@testing-library/vue';
 
 vi.mock('@/stores/workflows.store', () => ({
 	useWorkflowsStore: vi.fn().mockReturnValue({
@@ -45,6 +46,7 @@ vi.mock('@/stores/workflows.store', () => ({
 		getPinnedDataLastRemovedAt: vi.fn(),
 		incomingConnectionsByNodeName: vi.fn(),
 		outgoingConnectionsByNodeName: vi.fn(),
+		markExecutionAsStopped: vi.fn(),
 	}),
 }));
 
@@ -671,4 +673,41 @@ describe('useRunWorkflow({ router })', () => {
 			});
 		});
 	});
+
+	describe('stopCurrentExecution()', () => {
+		it('should not prematurely call markExecutionAsStopped() while execution status is still "running"', async () => {
+			const runWorkflowComposable = useRunWorkflow({ router });
+			const executionData: IExecutionResponse = {
+				id: 'test-exec-id',
+				workflowData: createTestWorkflow({ id: 'test-wf-id' }),
+				finished: false,
+				mode: 'manual',
+				status: 'running',
+				startedAt: new Date('2025-04-01T00:00:00.000Z'),
+				createdAt: new Date('2025-04-01T00:00:00.000Z'),
+			};
+			const markStoppedSpy = vi.spyOn(workflowsStore, 'markExecutionAsStopped');
+
+			workflowsStore.workflowExecutionData = executionData;
+			workflowsStore.activeWorkflows = ['test-wf-id'];
+			workflowsStore.activeExecutionId = 'test-exec-id';
+
+			// Exercise - don't wait for returned promise to resolve
+			void runWorkflowComposable.stopCurrentExecution();
+
+			// Assert that markExecutionAsStopped() isn't called yet after a simulated delay
+			await new Promise((resolve) => setTimeout(resolve, 10));
+			expect(markStoppedSpy).not.toHaveBeenCalled();
+
+			// Simulated executionFinished event
+			workflowsStore.workflowExecutionData = {
+				...executionData,
+				status: 'canceled',
+				stoppedAt: new Date('2025-04-01T00:00:99.000Z'),
+			};
+
+			// Assert that markExecutionAsStopped() is called eventually
+			await waitFor(() => expect(markStoppedSpy).toHaveBeenCalled());
+		});
+	});
 });
diff --git a/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts b/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts
index 30b499c8e4e72..f45b43dfc0dba 100644
--- a/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts
+++ b/packages/frontend/editor-ui/src/composables/useRunWorkflow.ts
@@ -447,6 +447,15 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
 				toast.showError(error, i18n.baseText('nodeView.showError.stopExecution.title'));
 			}
 		} finally {
+			// Wait for websocket event to update the execution status to 'canceled'
+			for (let i = 0; i < 100; i++) {
+				if (workflowsStore.workflowExecutionData?.status !== 'running') {
+					break;
+				}
+
+				await new Promise(requestAnimationFrame);
+			}
+
 			workflowsStore.markExecutionAsStopped();
 		}
 	}
diff --git a/packages/frontend/editor-ui/src/plugins/i18n/index.ts b/packages/frontend/editor-ui/src/plugins/i18n/index.ts
index f4c23317391c4..0bf6723721c4a 100644
--- a/packages/frontend/editor-ui/src/plugins/i18n/index.ts
+++ b/packages/frontend/editor-ui/src/plugins/i18n/index.ts
@@ -93,6 +93,10 @@ export class I18nClass {
 				return `${Math.floor(msPassed / 1000)}${this.baseText('genericHelpers.secShort')}`;
 			}
 
+			if (msPassed > 0 && msPassed < 1000) {
+				return `${msPassed}${this.baseText('genericHelpers.millis')}`;
+			}
+
 			return `${msPassed / 1000}${this.baseText('genericHelpers.secShort')}`;
 		}
 
diff --git a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
index b4569d2de053d..03a15305cf45f 100644
--- a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
@@ -963,6 +963,7 @@
 	"genericHelpers.minShort": "m",
 	"genericHelpers.sec": "sec",
 	"genericHelpers.secShort": "s",
+	"genericHelpers.millis": "ms",
 	"readOnly.showMessage.executions.message": "Executions are read-only. Make changes from the <b>Workflow</b> tab.",
 	"readOnly.showMessage.executions.title": "Cannot edit execution",
 	"readOnlyEnv.showMessage.executions.message": "Executions are read-only.",
@@ -979,8 +980,12 @@
 	"logs.overview.header.title": "Logs",
 	"logs.overview.header.actions.clearExecution": "Clear execution",
 	"logs.overview.header.actions.clearExecution.tooltip": "Clear execution data",
+	"logs.overview.header.switch.details": "Details",
+	"logs.overview.header.switch.overview": "Overview",
 	"logs.overview.body.empty.message": "Nothing to display yet. Execute the workflow to see execution logs.",
 	"logs.overview.body.empty.action": "Execute the workflow",
+	"logs.overview.body.summaryText": "{status} in {time}",
+	"logs.overview.body.started": "Started {time}",
 	"mainSidebar.aboutN8n": "About n8n",
 	"mainSidebar.confirmMessage.workflowDelete.cancelButtonText": "",
 	"mainSidebar.confirmMessage.workflowDelete.confirmButtonText": "Yes, delete",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7dd6960ffda34..a62cd485cceb3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -103,6 +103,9 @@ catalogs:
     '@vueuse/core':
       specifier: ^10.11.0
       version: 10.11.0
+    element-plus:
+      specifier: 2.4.3
+      version: 2.4.3
     highlight.js:
       specifier: ^11.8.0
       version: 11.9.0
@@ -494,7 +497,7 @@ importers:
         version: 3.666.0(@aws-sdk/client-sts@3.666.0)
       '@getzep/zep-cloud':
         specifier: 1.0.12
-        version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0))
+        version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a))
       '@getzep/zep-js':
         specifier: 0.9.0
         version: 0.9.0
@@ -521,7 +524,7 @@ importers:
         version: 0.3.2(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
       '@langchain/community':
         specifier: 0.3.24
-        version: 0.3.24(1725dd003b6ba0539bce135b7f30abed)
+        version: 0.3.24(14647e509198b6d5542cb42df21485e1)
       '@langchain/core':
         specifier: 'catalog:'
         version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
@@ -614,7 +617,7 @@ importers:
         version: 23.0.1
       langchain:
         specifier: 0.3.11
-        version: 0.3.11(fd386e1130022c8548c06dd951c5cbf0)
+        version: 0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a)
       lodash:
         specifier: 'catalog:'
         version: 4.17.21
@@ -1439,7 +1442,7 @@ importers:
         specifier: '*'
         version: 10.11.0(vue@3.5.13(typescript@5.8.2))
       element-plus:
-        specifier: 2.4.3
+        specifier: catalog:frontend
         version: 2.4.3(vue@3.5.13(typescript@5.8.2))
       is-emoji-supported:
         specifier: ^0.0.5
@@ -1682,6 +1685,9 @@ importers:
       dateformat:
         specifier: ^3.0.3
         version: 3.0.3
+      element-plus:
+        specifier: catalog:frontend
+        version: 2.4.3(vue@3.5.13(typescript@5.8.2))
       email-providers:
         specifier: ^2.0.1
         version: 2.0.1
@@ -8896,6 +8902,7 @@ packages:
   gm@1.25.0:
     resolution: {integrity: sha512-4kKdWXTtgQ4biIo7hZA396HT062nDVVHPjQcurNZ3o/voYN+o5FUC5kOwuORbpExp3XbTJ3SU7iRipiIhQtovw==}
     engines: {node: '>=14'}
+    deprecated: The gm module has been sunset. Please migrate to an alternative. https://github.com/aheckmann/gm?tab=readme-ov-file#2025-02-24-this-project-is-not-maintained
 
   google-auth-library@8.9.0:
     resolution: {integrity: sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==}
@@ -15973,7 +15980,7 @@ snapshots:
   '@gar/promisify@1.1.3':
     optional: true
 
-  '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0))':
+  '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a))':
     dependencies:
       form-data: 4.0.0
       node-fetch: 2.7.0(encoding@0.1.13)
@@ -15982,7 +15989,7 @@ snapshots:
       zod: 3.24.1
     optionalDependencies:
       '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
-      langchain: 0.3.11(fd386e1130022c8548c06dd951c5cbf0)
+      langchain: 0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a)
     transitivePeerDependencies:
       - encoding
 
@@ -16490,7 +16497,7 @@ snapshots:
       - aws-crt
       - encoding
 
-  '@langchain/community@0.3.24(1725dd003b6ba0539bce135b7f30abed)':
+  '@langchain/community@0.3.24(14647e509198b6d5542cb42df21485e1)':
     dependencies:
       '@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1)
       '@ibm-cloud/watsonx-ai': 1.1.2
@@ -16501,7 +16508,7 @@ snapshots:
       flat: 5.0.2
       ibm-cloud-sdk-core: 5.1.0
       js-yaml: 4.1.0
-      langchain: 0.3.11(fd386e1130022c8548c06dd951c5cbf0)
+      langchain: 0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a)
       langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
       openai: 4.78.1(encoding@0.1.13)(zod@3.24.1)
       uuid: 10.0.0
@@ -16516,7 +16523,7 @@ snapshots:
       '@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
       '@azure/storage-blob': 12.18.0(encoding@0.1.13)
       '@browserbasehq/sdk': 2.0.0(encoding@0.1.13)
-      '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0))
+      '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a))
       '@getzep/zep-js': 0.9.0
       '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
       '@google-cloud/storage': 7.12.1(encoding@0.1.13)
@@ -22668,7 +22675,7 @@ snapshots:
       '@types/debug': 4.1.12
       '@types/node': 18.16.16
       '@types/tough-cookie': 4.0.2
-      axios: 1.8.2(debug@4.4.0)
+      axios: 1.8.2
       camelcase: 6.3.0
       debug: 4.4.0(supports-color@8.1.1)
       dotenv: 16.4.5
@@ -22678,7 +22685,7 @@ snapshots:
       isstream: 0.1.2
       jsonwebtoken: 9.0.2
       mime-types: 2.1.35
-      retry-axios: 2.6.0(axios@1.8.2)
+      retry-axios: 2.6.0(axios@1.8.2(debug@4.4.0))
       tough-cookie: 4.1.3
     transitivePeerDependencies:
       - supports-color
@@ -23661,7 +23668,7 @@ snapshots:
 
   kuler@2.0.0: {}
 
-  langchain@0.3.11(fd386e1130022c8548c06dd951c5cbf0):
+  langchain@0.3.11(d1e86e144e3517fab3dbb7a92ab7f45a):
     dependencies:
       '@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
       '@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
@@ -26066,7 +26073,7 @@ snapshots:
 
   ret@0.1.15: {}
 
-  retry-axios@2.6.0(axios@1.8.2):
+  retry-axios@2.6.0(axios@1.8.2(debug@4.4.0)):
     dependencies:
       axios: 1.8.2
 
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index c46586e86dbab..ab6741d366240 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -50,3 +50,4 @@ catalogs:
     vue-tsc: ^2.2.8
     vue-markdown-render: ^2.2.1
     highlight.js: ^11.8.0
+    'element-plus': 2.4.3


Reviews:
user: login: autologie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:19:09Z
user: login: OlegIvaniv
body: Looks great! Left a few minor comments 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T16:13:17Z
user: login: autologie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T07:23:32Z
user: login: autologie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T08:53:04Z
user: login: OlegIvaniv
body: Thanks for addressing my comments, LGTM.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T11:06:49Z

Review comments:
diff_hunk: @@ -445,9 +445,9 @@ export interface IExecutionBase {
 	status: ExecutionStatus;
 	retryOf?: string;
 	retrySuccessId?: string;
-	startedAt: Date;
-	createdAt: Date;
-	stoppedAt?: Date;
+	startedAt: Date | string;
+	createdAt: Date | string;
+	stoppedAt?: Date | string;
path: packages/frontend/editor-ui/src/Interface.ts
user: login: autologie
body: Changing type to match the actual behavior (happens when the execution data is received from BE)
created_at: 2025-03-27T15:12:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,6 +59,7 @@
     "core-js": "^3.40.0",
     "curlconverter": "^4.12.0",
     "dateformat": "^3.0.3",
+    "element-plus": "2.4.3",
path: packages/frontend/editor-ui/package.json
user: login: autologie
body: This library has been already in use. I'm adding it as an explicit dependency.
created_at: 2025-03-27T15:14:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -240,10 +240,19 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
 
 			let showedSuccessToast = false;
 
-			let executionData: Pick<IExecutionResponse, 'workflowId' | 'data' | 'status'>;
+			let executionData: Pick<
+				IExecutionResponse,
+				'workflowId' | 'data' | 'status' | 'startedAt' | 'stoppedAt'
+			>;
 			if (receivedData.type === 'executionFinished' && receivedData.data.rawData) {
 				const { workflowId, status, rawData } = receivedData.data;
-				executionData = { workflowId, data: parse(rawData), status };
+				executionData = {
+					workflowId,
+					data: parse(rawData),
+					status,
+					startedAt: workflowsStore.workflowExecutionData?.startedAt as Date,
path: packages/frontend/editor-ui/src/composables/usePushConnection.ts
user: login: OlegIvaniv
body: Could we do a typeguard here instead of coercing it? Something like: 
```
function ensureDate(value: string | Date | undefined): Date | undefined {
		if (!value) return new Date();
		if (value instanceof Date) return value;
		return new Date(value);
	}
	
```
created_at: 2025-03-28T14:51:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,225 @@
+<script setup lang="ts">
+import { type TreeNode as ElTreeNode } from 'element-plus';
+import { getSubtreeTotalConsumedTokens, type TreeNode } from '@/components/RunDataAi/utils';
+import { useWorkflowsStore } from '@/stores/workflows.store';
+import { computed } from 'vue';
+import { type INodeUi } from '@/Interface';
+import { type ExecutionStatus, type ITaskData } from 'n8n-workflow';
+import { N8nIconButton, N8nText } from '@n8n/design-system';
+import { useNodeTypesStore } from '@/stores/nodeTypes.store';
+import { upperFirst } from 'lodash-es';
+import { useI18n } from '@/composables/useI18n';
+import ConsumedTokenCountText from '@/components/CanvasChat/future/components/ConsumedTokenCountText.vue';
+
+const props = defineProps<{
+	data: TreeNode;
+	node: ElTreeNode;
+	isSelected: boolean;
+	shouldShowConsumedTokens: boolean;
+}>();
+
+const locale = useI18n();
+const workflowsStore = useWorkflowsStore();
+const nodeTypeStore = useNodeTypesStore();
+const node = computed<INodeUi | undefined>(() => workflowsStore.nodesByName[props.data.node]);
+const runData = computed<ITaskData | undefined>(() =>
+	node.value
+		? workflowsStore.workflowExecutionData?.data?.resultData.runData[node.value.name]?.[
+				props.data.runIndex
+			]
+		: undefined,
+);
+const type = computed(() => (node.value ? nodeTypeStore.getNodeType(node.value.type) : undefined));
+const depth = computed(() => (props.node.level ?? 1) - 1);
+const timeTookText = computed(() => {
+	const finalStatuses: ExecutionStatus[] = ['crashed', 'error', 'success'];
+	const status = runData.value?.executionStatus;
+
+	if (!status) {
+		return '';
+	}
+
+	const statusText = upperFirst(status);
+
+	return finalStatuses.includes(status)
+		? locale.baseText('logs.overview.body.summaryText', {
+				interpolate: {
+					status: statusText,
+					time: locale.displayTimer(runData.value.executionTime, true),
+				},
+			})
+		: statusText;
+});
+const startedAtText = computed(() =>
+	locale.baseText('logs.overview.body.started', {
+		interpolate: {
+			time: new Date(runData.value?.startTime ?? 0).toISOString(), // TODO: confirm date format
+		},
+	}),
+);
+
+const subtreeConsumedTokens = computed(() =>
+	props.shouldShowConsumedTokens ? getSubtreeTotalConsumedTokens(props.data) : undefined,
+);
+
+function isLastChild(level: number) {
+	let parent = props.data.parent;
+	let data: TreeNode | undefined = props.data;
+
+	for (let i = 0; i < depth.value - level; i++) {
+		data = parent;
+		parent = parent?.parent;
+	}
+
+	const siblings = parent?.children ?? [];
+
+	return data === siblings[siblings.length - 1];
+}
+
+function handleClickToggleButton() {
+	props.node.expanded = !props.node.expanded;
path: packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue
user: login: OlegIvaniv
body: Could we emit an event here to not mutate the prop directly?
created_at: 2025-03-28T14:55:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,25 +1,96 @@
 <script setup lang="ts">
-import PanelHeader from '@/components/CanvasChat/components/PanelHeader.vue';
+import PanelHeader from '@/components/CanvasChat/future/components/PanelHeader.vue';
 import { useClearExecutionButtonVisible } from '@/composables/useClearExecutionButtonVisible';
 import { useI18n } from '@/composables/useI18n';
 import { useNodeHelpers } from '@/composables/useNodeHelpers';
 import { useWorkflowsStore } from '@/stores/workflows.store';
-import { N8nButton, N8nText, N8nTooltip } from '@n8n/design-system';
+import { N8nButton, N8nRadioButtons, N8nText, N8nTooltip } from '@n8n/design-system';
+import { computed, ref } from 'vue';
+import { ElTree } from 'element-plus';
+import {
+	createAiData,
+	getSubtreeTotalConsumedTokens,
+	getTotalConsumedTokens,
+	getTreeNodeData,
+	type TreeNode,
+} from '@/components/RunDataAi/utils';
+import { type INodeUi } from '@/Interface';
+import { upperFirst } from 'lodash-es';
+import { useTelemetry } from '@/composables/useTelemetry';
+import ConsumedTokenCountText from '@/components/CanvasChat/future/components/ConsumedTokenCountText.vue';
 
-defineProps<{ isOpen: boolean }>();
+const { node, isOpen } = defineProps<{ isOpen: boolean; node: INodeUi | null }>();
 
 const emit = defineEmits<{ clickHeader: [] }>();
+
+defineSlots<{ actions: {} }>();
+
 const locale = useI18n();
+const telemetry = useTelemetry();
 const workflowsStore = useWorkflowsStore();
 const nodeHelpers = useNodeHelpers();
 const isClearExecutionButtonVisible = useClearExecutionButtonVisible();
+const workflow = computed(() => workflowsStore.getCurrentWorkflow());
+const executionTree = computed<TreeNode[]>(() =>
+	node
+		? getTreeNodeData(
+				node.name,
+				workflow.value,
+				createAiData(node.name, workflow.value, workflowsStore.getWorkflowResultDataByNodeName),
+			)
+		: [],
+);
+const isEmpty = computed(() => workflowsStore.workflowExecutionData === null);
+const switchViewOptions = computed<Array<{ label: string; value: string }>>(() => [
+	{ label: 'Details', value: 'details' },
+	{ label: 'Overview', value: 'overview' },
path: packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue
user: login: OlegIvaniv
body: i18n 👁️ 
created_at: 2025-03-28T15:14:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,225 @@
+<script setup lang="ts">
+import { type TreeNode as ElTreeNode } from 'element-plus';
+import { getSubtreeTotalConsumedTokens, type TreeNode } from '@/components/RunDataAi/utils';
+import { useWorkflowsStore } from '@/stores/workflows.store';
+import { computed } from 'vue';
+import { type INodeUi } from '@/Interface';
+import { type ExecutionStatus, type ITaskData } from 'n8n-workflow';
+import { N8nIconButton, N8nText } from '@n8n/design-system';
+import { useNodeTypesStore } from '@/stores/nodeTypes.store';
+import { upperFirst } from 'lodash-es';
+import { useI18n } from '@/composables/useI18n';
+import ConsumedTokenCountText from '@/components/CanvasChat/future/components/ConsumedTokenCountText.vue';
+
+const props = defineProps<{
+	data: TreeNode;
+	node: ElTreeNode;
+	isSelected: boolean;
+	shouldShowConsumedTokens: boolean;
+}>();
+
+const locale = useI18n();
+const workflowsStore = useWorkflowsStore();
+const nodeTypeStore = useNodeTypesStore();
+const node = computed<INodeUi | undefined>(() => workflowsStore.nodesByName[props.data.node]);
+const runData = computed<ITaskData | undefined>(() =>
+	node.value
+		? workflowsStore.workflowExecutionData?.data?.resultData.runData[node.value.name]?.[
+				props.data.runIndex
+			]
+		: undefined,
+);
+const type = computed(() => (node.value ? nodeTypeStore.getNodeType(node.value.type) : undefined));
+const depth = computed(() => (props.node.level ?? 1) - 1);
+const timeTookText = computed(() => {
+	const finalStatuses: ExecutionStatus[] = ['crashed', 'error', 'success'];
+	const status = runData.value?.executionStatus;
+
+	if (!status) {
+		return '';
+	}
+
+	const statusText = upperFirst(status);
+
+	return finalStatuses.includes(status)
+		? locale.baseText('logs.overview.body.summaryText', {
+				interpolate: {
+					status: statusText,
+					time: locale.displayTimer(runData.value.executionTime, true),
+				},
+			})
+		: statusText;
+});
+const startedAtText = computed(() =>
+	locale.baseText('logs.overview.body.started', {
+		interpolate: {
+			time: new Date(runData.value?.startTime ?? 0).toISOString(), // TODO: confirm date format
+		},
+	}),
+);
+
+const subtreeConsumedTokens = computed(() =>
+	props.shouldShowConsumedTokens ? getSubtreeTotalConsumedTokens(props.data) : undefined,
+);
+
+function isLastChild(level: number) {
+	let parent = props.data.parent;
+	let data: TreeNode | undefined = props.data;
+
+	for (let i = 0; i < depth.value - level; i++) {
+		data = parent;
+		parent = parent?.parent;
+	}
+
+	const siblings = parent?.children ?? [];
+
+	return data === siblings[siblings.length - 1];
+}
+
+function handleClickToggleButton() {
+	props.node.expanded = !props.node.expanded;
+}
+</script>
+
+<template>
+	<div v-if="node !== undefined" :class="$style.container">
+		<template v-for="level in depth" :key="level">
+			<div
+				:class="{
+					[$style.indent]: true,
+					[$style.connectorCurved]: level === depth,
+					[$style.connectorStraight]: !isLastChild(level),
+				}"
+			/>
+		</template>
+		<div :class="[$style.selectable, props.isSelected ? $style.selected : '']">
+			<NodeIcon :node-type="type" :size="16" :class="$style.icon" />
+			<N8nText tag="div" :bold="true" size="small" :class="$style.name">{{ node.name }}</N8nText>
+			<N8nText tag="div" color="text-light" size="small" :class="$style.timeTook">{{
+				timeTookText
+			}}</N8nText>
+			<N8nText tag="div" color="text-light" size="small" :class="$style.startedAt">{{
+				startedAtText
+			}}</N8nText>
+			<div v-if="subtreeConsumedTokens !== undefined" :class="$style.consumedTokens">
+				<ConsumedTokenCountText
+					v-if="
+						subtreeConsumedTokens.totalTokens > 0 &&
+						(props.data.children.length === 0 || !props.node.expanded)
+					"
+					:consumed-tokens="subtreeConsumedTokens"
+				/>
+			</div>
+			<N8nIconButton
+				type="secondary"
+				size="medium"
+				:icon="props.node.expanded ? 'chevron-down' : 'chevron-up'"
+				:style="{
+					visibility: props.data.children.length === 0 ? 'hidden' : '',
+					color: 'var(--color-text-base)',
path: packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue
user: login: OlegIvaniv
body: Should we put this to toggleButton class? Also perhaps use v-show instead of visibility?
created_at: 2025-03-28T15:15:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,225 @@
+<script setup lang="ts">
+import { type TreeNode as ElTreeNode } from 'element-plus';
+import { getSubtreeTotalConsumedTokens, type TreeNode } from '@/components/RunDataAi/utils';
+import { useWorkflowsStore } from '@/stores/workflows.store';
+import { computed } from 'vue';
+import { type INodeUi } from '@/Interface';
+import { type ExecutionStatus, type ITaskData } from 'n8n-workflow';
+import { N8nIconButton, N8nText } from '@n8n/design-system';
+import { useNodeTypesStore } from '@/stores/nodeTypes.store';
+import { upperFirst } from 'lodash-es';
+import { useI18n } from '@/composables/useI18n';
+import ConsumedTokenCountText from '@/components/CanvasChat/future/components/ConsumedTokenCountText.vue';
+
+const props = defineProps<{
+	data: TreeNode;
+	node: ElTreeNode;
+	isSelected: boolean;
+	shouldShowConsumedTokens: boolean;
+}>();
+
+const locale = useI18n();
+const workflowsStore = useWorkflowsStore();
+const nodeTypeStore = useNodeTypesStore();
+const node = computed<INodeUi | undefined>(() => workflowsStore.nodesByName[props.data.node]);
+const runData = computed<ITaskData | undefined>(() =>
+	node.value
+		? workflowsStore.workflowExecutionData?.data?.resultData.runData[node.value.name]?.[
+				props.data.runIndex
+			]
+		: undefined,
+);
+const type = computed(() => (node.value ? nodeTypeStore.getNodeType(node.value.type) : undefined));
+const depth = computed(() => (props.node.level ?? 1) - 1);
+const timeTookText = computed(() => {
+	const finalStatuses: ExecutionStatus[] = ['crashed', 'error', 'success'];
+	const status = runData.value?.executionStatus;
+
+	if (!status) {
+		return '';
+	}
+
+	const statusText = upperFirst(status);
+
+	return finalStatuses.includes(status)
+		? locale.baseText('logs.overview.body.summaryText', {
+				interpolate: {
+					status: statusText,
+					time: locale.displayTimer(runData.value.executionTime, true),
+				},
+			})
+		: statusText;
+});
+const startedAtText = computed(() =>
+	locale.baseText('logs.overview.body.started', {
+		interpolate: {
+			time: new Date(runData.value?.startTime ?? 0).toISOString(), // TODO: confirm date format
+		},
+	}),
+);
+
+const subtreeConsumedTokens = computed(() =>
+	props.shouldShowConsumedTokens ? getSubtreeTotalConsumedTokens(props.data) : undefined,
+);
+
+function isLastChild(level: number) {
+	let parent = props.data.parent;
+	let data: TreeNode | undefined = props.data;
+
+	for (let i = 0; i < depth.value - level; i++) {
+		data = parent;
+		parent = parent?.parent;
+	}
+
+	const siblings = parent?.children ?? [];
+
+	return data === siblings[siblings.length - 1];
+}
+
+function handleClickToggleButton() {
+	props.node.expanded = !props.node.expanded;
+}
+</script>
+
+<template>
+	<div v-if="node !== undefined" :class="$style.container">
+		<template v-for="level in depth" :key="level">
+			<div
+				:class="{
+					[$style.indent]: true,
+					[$style.connectorCurved]: level === depth,
+					[$style.connectorStraight]: !isLastChild(level),
+				}"
+			/>
+		</template>
+		<div :class="[$style.selectable, props.isSelected ? $style.selected : '']">
+			<NodeIcon :node-type="type" :size="16" :class="$style.icon" />
+			<N8nText tag="div" :bold="true" size="small" :class="$style.name">{{ node.name }}</N8nText>
+			<N8nText tag="div" color="text-light" size="small" :class="$style.timeTook">{{
+				timeTookText
+			}}</N8nText>
+			<N8nText tag="div" color="text-light" size="small" :class="$style.startedAt">{{
+				startedAtText
+			}}</N8nText>
+			<div v-if="subtreeConsumedTokens !== undefined" :class="$style.consumedTokens">
+				<ConsumedTokenCountText
+					v-if="
+						subtreeConsumedTokens.totalTokens > 0 &&
+						(props.data.children.length === 0 || !props.node.expanded)
+					"
+					:consumed-tokens="subtreeConsumedTokens"
+				/>
+			</div>
+			<N8nIconButton
+				type="secondary"
+				size="medium"
+				:icon="props.node.expanded ? 'chevron-down' : 'chevron-up'"
+				:style="{
+					visibility: props.data.children.length === 0 ? 'hidden' : '',
+					color: 'var(--color-text-base)',
path: packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue
user: login: autologie
body: For visibility: `v-show` toggles `display` but here I wanted to preserve its size so that columns are aligned.
For color: this is to override the color from design system, and has to have higher specificity than styling via class (will leave a comment on this).
created_at: 2025-03-31T07:23:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -240,10 +240,19 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
 
 			let showedSuccessToast = false;
 
-			let executionData: Pick<IExecutionResponse, 'workflowId' | 'data' | 'status'>;
+			let executionData: Pick<
+				IExecutionResponse,
+				'workflowId' | 'data' | 'status' | 'startedAt' | 'stoppedAt'
+			>;
 			if (receivedData.type === 'executionFinished' && receivedData.data.rawData) {
 				const { workflowId, status, rawData } = receivedData.data;
-				executionData = { workflowId, data: parse(rawData), status };
+				executionData = {
+					workflowId,
+					data: parse(rawData),
+					status,
+					startedAt: workflowsStore.workflowExecutionData?.startedAt as Date,
path: packages/frontend/editor-ui/src/composables/usePushConnection.ts
user: login: autologie
body: Thanks. Only `undefined` is the problem here, so I just replaced coercing with `?? new Date()`
created_at: 2025-03-31T08:53:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-19T10:01:12Z
body: ## [Codecov](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) Report
Attention: Patch coverage is `84.61538%` with `50 lines` in your changes missing coverage. Please review.
| [Files with missing lines](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) | Patch % | Lines |
|---|---|---|
| [...CanvasChat/future/components/LogsOverviewPanel.vue](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvZnJvbnRlbmQvZWRpdG9yLXVpL3NyYy9jb21wb25lbnRzL0NhbnZhc0NoYXQvZnV0dXJlL2NvbXBvbmVudHMvTG9nc092ZXJ2aWV3UGFuZWwudnVl) | 78.21% | [22 Missing :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |
| [...s/CanvasChat/future/components/LogsOverviewRow.vue](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvZnJvbnRlbmQvZWRpdG9yLXVpL3NyYy9jb21wb25lbnRzL0NhbnZhc0NoYXQvZnV0dXJlL2NvbXBvbmVudHMvTG9nc092ZXJ2aWV3Um93LnZ1ZQ==) | 82.65% | [15 Missing and 2 partials :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |
| [...end/editor-ui/src/composables/usePushConnection.ts](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvZnJvbnRlbmQvZWRpdG9yLXVpL3NyYy9jb21wb3NhYmxlcy91c2VQdXNoQ29ubmVjdGlvbi50cw==) | 70.00% | [3 Missing :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |
| [...ontend/editor-ui/src/composables/useRunWorkflow.ts](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvZnJvbnRlbmQvZWRpdG9yLXVpL3NyYy9jb21wb3NhYmxlcy91c2VSdW5Xb3JrZmxvdy50cw==) | 50.00% | [3 Missing :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |
| [...kages/frontend/editor-ui/src/plugins/i18n/index.ts](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvZnJvbnRlbmQvZWRpdG9yLXVpL3NyYy9wbHVnaW5zL2kxOG4vaW5kZXgudHM=) | 0.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |
| [...sChat/future/components/ConsumedTokenCountText.vue](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvZnJvbnRlbmQvZWRpdG9yLXVpL3NyYy9jb21wb25lbnRzL0NhbnZhc0NoYXQvZnV0dXJlL2NvbXBvbmVudHMvQ29uc3VtZWRUb2tlbkNvdW50VGV4dC52dWU=) | 91.66% | [1 Missing :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |
| [...ontend/editor-ui/src/components/RunDataAi/utils.ts](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvZnJvbnRlbmQvZWRpdG9yLXVpL3NyYy9jb21wb25lbnRzL1J1bkRhdGFBaS91dGlscy50cw==) | 98.41% | [1 Missing :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14045?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE
user: login: cypress[bot]
created_at: 2025-03-31T11:15:36Z
body: 
<h3>
  <a href="https://cloud.cypress.io/projects/5hbsdn?utm_source=github&utm_medium=flaky&utm_campaign=project">n8n</a>&nbsp;
  <img src="https://assets.cypress.io/github-pr-comment-icons/chevron.svg" alt="" height="14" width="14" />&nbsp;
  <a href="https://cloud.cypress.io/projects/5hbsdn/runs/10015/overview?utm_source=github&utm_medium=flaky&utm_campaign=run%20number">Run #10015</a>
</h3>

<details>
  <summary>
    <strong>Run Properties:</strong>&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/5hbsdn/runs/10015/overview?utm_source=github&utm_medium=flaky&utm_campaign=run%20number">#10015</a></code> &nbsp;&bull;&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/n8n-io/n8n/commit/6ac087f156469cb7176d5f133111cb18f8334b5e">6ac087f156</a>: 🌳  🖥️ browsers:node18.12.0-chrome107 🤖 autologie 🗃️ e2e/*</code>
  </summary><br />

  <table width="744">
    <tbody>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Project</strong>
        </td>
        <td width="520">
          <code><a href="https://cloud.cypress.io/projects/5hbsdn?utm_source=github&utm_medium=flaky&utm_campaign=project">n8n</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Branch Review</strong>
        </th>
        <td>
          <code><a href="https://cloud.cypress.io/projects/5hbsdn/branches/sug-23-logs-overview/review?utm_source=github&utm_medium=flaky&utm_campaign=review%20changes&changeRequest=14045">sug-23-logs-overview</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run status</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/5hbsdn/runs/10015/overview?utm_source=github&utm_medium=flaky&utm_campaign=run%20number">#10015</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run duration</strong></td>
        <td><code>03m 47s</code></td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Commit</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/n8n-io/n8n/commit/6ac087f156469cb7176d5f133111cb18f8334b5e">6ac087f156</a>: 🌳  🖥️ browsers:node18.12.0-chrome107 🤖 autologie 🗃️ e2e/*</code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Committer</strong></td>
        <td><code>autologie</code></td>
      </tr>
    </tbody>
    <tfoot>
      <tr />
      <tr height="48">
        <td colspan="2">
          <a href="https://cloud.cypress.io/projects/5hbsdn/runs/10015/properties?utm_source=github&utm_medium=flaky&utm_campaign=properties">View all properties for this run ↗︎</a>
        </td>
      </tr>
    </tfoot>
  </table>
  <hr />
</details>

<table width="744">
  <thead>
    <tr />
    <tr height="48">
      <th colspan="2" align="left" width="744">Test results</th>
    </tr>
  </thead>
  <tbody>
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/failed.svg" alt="Tests that failed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/10015/overview?reviewViewBy=FAILED&utm_source=github&utm_medium=flaky&utm_campaign=failed%20tests">
          <strong>Failures</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/flaky-v3.svg" alt="Tests that were flaky" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/10015/overview?reviewViewBy=FLAKY&utm_source=github&utm_medium=flaky&utm_campaign=flaky%20tests">
          <strong>Flaky</strong>
        </a>
      </td>
      <td>
        <code><strong>1</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/pending.svg" alt="Tests that did not run due to a developer annotating a test with .skip" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/10015/test-results?utm_source=github&utm_medium=flaky&utm_campaign=pending%20tests&statuses=%5B%7B%22value%22%3A%22PENDING%22%2C%22label%22%3A%22PENDING%22%7D%5D">
          <strong>Pending</strong>
        </a>
      </td>
      <td>
        <code><strong>5</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/skipped-v3.svg" alt="Tests that did not run due to a failure in a mocha hook" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/10015/test-results?utm_source=github&utm_medium=flaky&utm_campaign=skipped%20tests&statuses=%5B%7B%22value%22%3A%22SKIPPED%22%2C%22label%22%3A%22SKIPPED%22%7D%5D">
          <strong>Skipped</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="Tests that passed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/10015/overview?utm_source=github&utm_medium=flaky&utm_campaign=passed%20tests">
          <strong>Passing</strong>
        </a>
      </td>
      <td>
        <code><strong>475</strong></code>
      </td>
    </tr><tfoot>
    <tr height="48"><td colspan="2"><a href="https://cloud.cypress.io/projects/5hbsdn/branches/sug-23-logs-overview/review?utm_source=github&utm_medium=flaky&utm_campaign=review%20changes&changeRequest=14045">View all changes introduced in this branch ↗︎</a></td></tr></tfoot>
  </tbody>
</table>






author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-31T11:15:39Z
body: :white_check_mark: All Cypress E2E specs passed
author_association: CONTRIBUTOR
user: login: janober
created_at: 2025-03-31T15:18:34Z
body: Got released with [`n8n@1.86.0`](https://github.com/n8n-io/n8n/releases/tag/n8n%401.86.0)
author_association: MEMBER