{{ i18n.baseText('agentSessions.timeline.requestDetails') }}
diff --git a/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineChart.vue b/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineChart.vue
index 5ad5aa0acf8..e34cac33059 100644
--- a/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineChart.vue
+++ b/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineChart.vue
@@ -7,9 +7,11 @@ import { convertToDisplayDate } from '@/app/utils/formatters/dateFormatter';
import type { CSSProperties } from 'vue';
import type { IdleRange, TimelineItem } from '../session-timeline.types';
import {
+ executionErrorLabel,
+ executionErrorMessage,
formatDuration,
hitlTimelineName,
- isErroredToolCallTimelineItem,
+ isErroredTimelineItem,
isSubAgentTimelineItem,
matchesTimelineFilters,
timelineItemStatus,
@@ -114,6 +116,8 @@ function popoverLabel(item: TimelineItem): string {
return i18n.baseText('agentSessions.timeline.workflow');
case 'node':
return i18n.baseText('agentSessions.timeline.node');
+ case 'execution-error':
+ return executionErrorLabel(item, i18n);
case 'suspension':
return i18n.baseText(
item.hitlRequestType === 'approval'
@@ -142,6 +146,8 @@ function popoverName(item: TimelineItem): string {
return item.workflowName ?? formatToolNameForDisplay(item.toolName);
case 'node':
return item.nodeDisplayName ?? formatToolNameForDisplay(item.toolName);
+ case 'execution-error':
+ return executionErrorMessage(item, i18n);
case 'suspension':
case 'hitl-response':
return hitlTimelineName(item, i18n);
@@ -351,7 +357,9 @@ onBeforeUnmount(() => {
:data-test-id="
activePopoverStatus.kind === 'hitl-response'
? 'timeline-popover-hitl-response-badge'
- : 'timeline-popover-tool-error-badge'
+ : activePopover.segment.item.kind === 'execution-error'
+ ? 'timeline-popover-execution-error-badge'
+ : 'timeline-popover-tool-error-badge'
"
>
{{ i18n.baseText(activePopoverStatus.labelKey) }}
@@ -384,13 +392,9 @@ onBeforeUnmount(() => {
type="button"
data-test-id="timeline-block"
:data-timeline-index="seg.index"
- :data-error="isErroredToolCallTimelineItem(seg.item) ? 'true' : undefined"
+ :data-error="isErroredTimelineItem(seg.item) ? 'true' : undefined"
:aria-label="blockAriaLabel(seg.item)"
- :class="[
- $style.block,
- props.selectedIndex === seg.index && $style.selected,
- isErroredToolCallTimelineItem(seg.item) && $style.error,
- ]"
+ :class="[$style.block, props.selectedIndex === seg.index && $style.selected]"
:data-selected="props.selectedIndex === seg.index ? 'true' : undefined"
:style="eventStyle(seg.item)"
@mouseenter="showPopover(seg, $event)"
@@ -506,16 +510,6 @@ onBeforeUnmount(() => {
z-index: 2;
}
-.error {
- outline: var(--focus--border-width) solid var(--border-color--danger);
- outline-offset: var(--spacing--5xs);
- z-index: 1;
-}
-
-.selected.error {
- z-index: 2;
-}
-
/*
* Keep the shared hover card compact so the single-line row layout
* (pill · name · duration · time) doesn't wrap.
diff --git a/packages/frontend/editor-ui/src/features/agents/components/SessionTimelinePill.vue b/packages/frontend/editor-ui/src/features/agents/components/SessionTimelinePill.vue
index f70b102842e..5c062f4594a 100644
--- a/packages/frontend/editor-ui/src/features/agents/components/SessionTimelinePill.vue
+++ b/packages/frontend/editor-ui/src/features/agents/components/SessionTimelinePill.vue
@@ -29,6 +29,8 @@ const icon = computed((): IconName => {
return 'workflow';
case 'node':
return 'box';
+ case 'execution-error':
+ return 'circle-x';
case 'suspension':
case 'idle':
return 'clock';
diff --git a/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineRow.vue b/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineRow.vue
index 5b2dad60496..ba4816eec1f 100644
--- a/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineRow.vue
+++ b/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineRow.vue
@@ -8,6 +8,8 @@ import { convertToDisplayDate } from '@/app/utils/formatters/dateFormatter';
import { VIEWS } from '@/app/constants/navigation';
import type { TimelineItem } from '../session-timeline.types';
import {
+ executionErrorLabel,
+ executionErrorMessage,
hitlTimelineName,
isSubAgentTimelineItem,
timelineItemStatus,
@@ -56,6 +58,8 @@ const infoText = computed((): string => {
return it.workflowName ?? formatToolNameForDisplay(it.toolName);
case 'node':
return it.nodeDisplayName ?? formatToolNameForDisplay(it.toolName);
+ case 'execution-error':
+ return executionErrorMessage(it, i18n);
case 'suspension':
case 'hitl-response':
return hitlTimelineName(it, i18n);
@@ -89,6 +93,8 @@ const label = computed((): string => {
return i18n.baseText('agentSessions.timeline.workflow');
case 'node':
return i18n.baseText('agentSessions.timeline.node');
+ case 'execution-error':
+ return executionErrorLabel(props.item, i18n);
case 'suspension':
return i18n.baseText(
props.item.hitlRequestType === 'approval'
@@ -130,7 +136,9 @@ const label = computed((): string => {
:data-test-id="
status.kind === 'hitl-response'
? 'timeline-hitl-response-badge'
- : 'timeline-tool-error-badge'
+ : item.kind === 'execution-error'
+ ? 'timeline-execution-error-badge'
+ : 'timeline-tool-error-badge'
"
>
{{ i18n.baseText(status.labelKey) }}
diff --git a/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineTable.vue b/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineTable.vue
index 1d80fd96b57..0e786f76b99 100644
--- a/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineTable.vue
+++ b/packages/frontend/editor-ui/src/features/agents/components/SessionTimelineTable.vue
@@ -38,6 +38,10 @@ function labelForKey(key: string): string {
return i18n.baseText('agentSessions.timeline.workflow');
case 'node':
return i18n.baseText('agentSessions.timeline.node');
+ case 'execution-error':
+ return i18n.baseText('agentSessions.timeline.executionFailed');
+ case 'execution-interrupted':
+ return i18n.baseText('agentSessions.timeline.executionInterrupted');
case 'suspension':
return i18n.baseText('agentSessions.timeline.hitlRequest');
case 'hitl-response':
diff --git a/packages/frontend/editor-ui/src/features/agents/composables/useAgentThreadsApi.ts b/packages/frontend/editor-ui/src/features/agents/composables/useAgentThreadsApi.ts
index 3843b074bce..c947ccbc304 100644
--- a/packages/frontend/editor-ui/src/features/agents/composables/useAgentThreadsApi.ts
+++ b/packages/frontend/editor-ui/src/features/agents/composables/useAgentThreadsApi.ts
@@ -23,10 +23,29 @@ export interface AgentExecutionThread {
firstMessage?: string | null;
/** Earliest non-null execution source for the thread (e.g. slack, telegram). */
source?: string | null;
+ failureSummary?: ThreadFailureSummary | null;
+ status?: AgentExecutionStatus | null;
}
export type AgentExecutionStatus = 'running' | 'success' | 'error' | 'cancelled' | 'interrupted';
export type AgentExecutionHitlStatus = 'suspended' | 'resumed';
+export type AgentExecutionFailureKind = 'execution' | 'tool' | 'node' | 'workflow';
+
+export interface AgentExecutionFailure {
+ kind: AgentExecutionFailureKind;
+ name: string | null;
+ message: string | null;
+ occurredAt: number;
+}
+
+export interface AgentExecutionFailureSummary {
+ count: number;
+ latest: AgentExecutionFailure;
+}
+
+export interface ThreadFailureSummary extends AgentExecutionFailureSummary {
+ latest: AgentExecutionFailure & { executionId: string };
+}
/**
* Raw timeline event shape as persisted on the agent_execution row.
@@ -62,6 +81,7 @@ export interface AgentExecution {
cost: number | null;
timeline: AgentExecutionTimelineEvent[] | null;
error: string | null;
+ failureSummary: AgentExecutionFailureSummary | null;
hitlStatus: AgentExecutionHitlStatus | null;
source: string | null;
}
diff --git a/packages/frontend/editor-ui/src/features/agents/session-timeline.styles.ts b/packages/frontend/editor-ui/src/features/agents/session-timeline.styles.ts
index 2bdae8bea15..474a0fba28f 100644
--- a/packages/frontend/editor-ui/src/features/agents/session-timeline.styles.ts
+++ b/packages/frontend/editor-ui/src/features/agents/session-timeline.styles.ts
@@ -19,6 +19,11 @@ export function pillColors(
return { backgroundColor: 'var(--color--orange-200)', color: 'var(--color--orange-950)' };
case 'node':
return { backgroundColor: 'var(--color--neutral-200)', color: 'var(--color--neutral-950)' };
+ case 'execution-error':
+ return {
+ backgroundColor: 'var(--color--red-150)',
+ color: 'var(--text-color--danger)',
+ };
case 'suspension':
case 'idle':
return { backgroundColor: 'var(--color--yellow-200)', color: 'var(--color--yellow-950)' };
diff --git a/packages/frontend/editor-ui/src/features/agents/session-timeline.types.ts b/packages/frontend/editor-ui/src/features/agents/session-timeline.types.ts
index 837c2dc64d9..6f9f45c1b73 100644
--- a/packages/frontend/editor-ui/src/features/agents/session-timeline.types.ts
+++ b/packages/frontend/editor-ui/src/features/agents/session-timeline.types.ts
@@ -4,6 +4,7 @@ export type EventKind =
| 'tool'
| 'node'
| 'workflow'
+ | 'execution-error'
| 'suspension'
| 'hitl-response';
@@ -24,6 +25,7 @@ export interface TimelineItem {
toolCallId?: string;
toolInput?: unknown;
toolOutput?: unknown;
+ executionStatus?: 'error' | 'interrupted';
/** Terminal outcome of a tool execution. Human decisions are represented on HITL response items. */
toolOutcome?: ToolCallOutcome;
/** @deprecated Use `toolOutcome`. Kept for compatibility with existing timeline consumers. */
diff --git a/packages/frontend/editor-ui/src/features/agents/session-timeline.utils.ts b/packages/frontend/editor-ui/src/features/agents/session-timeline.utils.ts
index 0d4ca9cbbf0..a191e563d05 100644
--- a/packages/frontend/editor-ui/src/features/agents/session-timeline.utils.ts
+++ b/packages/frontend/editor-ui/src/features/agents/session-timeline.utils.ts
@@ -88,6 +88,10 @@ export function isErroredToolCallTimelineItem(item: TimelineItem): boolean {
);
}
+export function isErroredTimelineItem(item: TimelineItem): boolean {
+ return item.kind === 'execution-error' || isErroredToolCallTimelineItem(item);
+}
+
/** Extracts a human-readable error message from a failed item's tool output. */
export function timelineItemErrorMessage(item: TimelineItem): string {
if (!isErroredToolCallTimelineItem(item)) return '';
@@ -105,6 +109,23 @@ export function hitlTimelineNameKey(item: TimelineItem): BaseTextKey | undefined
type TimelineI18n = Pick
, 'baseText'>;
+export function executionErrorLabel(item: TimelineItem, i18n: TimelineI18n): string {
+ return i18n.baseText(
+ item.executionStatus === 'interrupted'
+ ? 'agentSessions.timeline.executionInterrupted'
+ : 'agentSessions.timeline.executionFailed',
+ );
+}
+
+export function executionErrorMessage(item: TimelineItem, i18n: TimelineI18n): string {
+ if (item.content) return item.content;
+ return i18n.baseText(
+ item.executionStatus === 'interrupted'
+ ? 'agentSessions.timeline.executionInterruptedFallback'
+ : 'agentSessions.timeline.executionFailedFallback',
+ );
+}
+
export function linkedToolDisplayName(item: TimelineItem, i18n: TimelineI18n): string {
return (
item.hitlToolDisplayName ??
@@ -144,7 +165,7 @@ export function timelineItemStatus(item: TimelineItem): TimelineItemStatus | und
theme: 'default',
};
}
- if (isErroredToolCallTimelineItem(item)) {
+ if (isErroredTimelineItem(item)) {
return { kind: 'tool-error', labelKey: 'agentSessions.timeline.error', theme: 'danger' };
}
return undefined;
@@ -173,7 +194,7 @@ export function itemFilterKey(item: TimelineItem): string {
}
export function itemStatusFilterKey(item: TimelineItem): TimelineStatusFilterKey | undefined {
- if (isErroredToolCallTimelineItem(item)) return 'error';
+ if (isErroredTimelineItem(item)) return 'error';
if (
item.kind === 'hitl-response' &&
(item.hitlResponseStatus === 'approved' || item.hitlResponseStatus === 'declined')
@@ -213,6 +234,9 @@ export function timelineItemSearchText(
const parts: Array = [];
parts.push(labelForKey(itemFilterKey(item)));
+ if (item.kind === 'execution-error' && item.executionStatus === 'interrupted') {
+ parts.push(labelForKey('execution-interrupted'));
+ }
if (item.kind === 'suspension') {
parts.push(
labelForKey(item.hitlRequestType === 'approval' ? 'approval-requested' : 'hitl-requested'),
@@ -224,7 +248,7 @@ export function timelineItemSearchText(
if (item.hitlResponseStatus) {
parts.push(labelForKey(item.hitlResponseStatus));
}
- if (isErroredToolCallTimelineItem(item)) {
+ if (isErroredTimelineItem(item)) {
parts.push(labelForKey('error'));
}
@@ -294,6 +318,7 @@ const COLOR_MAP: Record = {
tool: 'var(--color--success)',
node: 'var(--color--text)',
workflow: 'var(--color--primary)',
+ 'execution-error': 'var(--color--danger)',
suspension: 'var(--color--warning)',
'hitl-response': 'var(--color--blue-400)',
};
@@ -308,6 +333,7 @@ const CHART_BLOCK_COLOR_MAP: Record = {
tool: 'var(--color--green-600)',
node: 'var(--color--neutral-600)',
workflow: 'var(--color--orange-600)',
+ 'execution-error': 'var(--color--red-400)',
suspension: 'var(--color--yellow-600)',
'hitl-response': 'var(--color--blue-600)',
};
@@ -618,6 +644,16 @@ export function flattenExecutionsToTimelineItems(executions: AgentExecution[]):
items.push(hitlResponseItem(hitlContext, exec.id, event.response, event.timestamp ?? 0));
}
}
+ if (exec.status === 'error' || exec.status === 'interrupted') {
+ const terminalTimestamp = exec.stoppedAt ?? exec.startedAt ?? exec.createdAt;
+ items.push({
+ kind: 'execution-error',
+ executionId: exec.id,
+ executionStatus: exec.status,
+ content: exec.error ?? undefined,
+ timestamp: terminalTimestamp ? new Date(terminalTimestamp).getTime() : 0,
+ });
+ }
}
return items;
}
diff --git a/packages/frontend/editor-ui/src/features/agents/views/AgentSessionTimelineView.vue b/packages/frontend/editor-ui/src/features/agents/views/AgentSessionTimelineView.vue
index 4c32c720839..204b1d2b260 100644
--- a/packages/frontend/editor-ui/src/features/agents/views/AgentSessionTimelineView.vue
+++ b/packages/frontend/editor-ui/src/features/agents/views/AgentSessionTimelineView.vue
@@ -95,6 +95,9 @@ const triggerIcon = computed((): IconName => {
const triggerLabel = computed((): string => {
const source = triggerSource.value;
if (!source) return '';
+ if (source === 'chat' || source === 'n8n_chat') {
+ return i18n.baseText('agentSessions.origin.preview');
+ }
return source.charAt(0).toUpperCase() + source.slice(1);
});
diff --git a/packages/frontend/editor-ui/src/features/agents/views/AgentSessionsListView.vue b/packages/frontend/editor-ui/src/features/agents/views/AgentSessionsListView.vue
index 970e164d233..99204d5dc71 100644
--- a/packages/frontend/editor-ui/src/features/agents/views/AgentSessionsListView.vue
+++ b/packages/frontend/editor-ui/src/features/agents/views/AgentSessionsListView.vue
@@ -6,12 +6,15 @@ import { convertToDisplayDate } from '@/app/utils/formatters/dateFormatter';
import { useAgentSessionsStore } from '@/features/agents/agentSessions.store';
import { AGENT_SESSION_DETAIL_VIEW } from '@/features/agents/constants';
import { useThreadTitle } from '@/features/agents/utils/thread-title';
-import type { AgentExecutionThread } from '@/features/agents/composables/useAgentThreadsApi';
+import type {
+ AgentExecutionStatus,
+ AgentExecutionThread,
+} from '@/features/agents/composables/useAgentThreadsApi';
import { useI18n } from '@n8n/i18n';
import { computed, onBeforeUnmount, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
-import { N8nActionDropdown, N8nButton, N8nIcon, N8nTableBase } from '@n8n/design-system';
+import { N8nActionDropdown, N8nButton, N8nIcon, N8nTableBase, N8nText } from '@n8n/design-system';
import type { ActionDropdownItem, IconName } from '@n8n/design-system';
import { ElSkeletonItem } from 'element-plus';
@@ -92,6 +95,28 @@ function formatDuration(ms: number): string {
return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
}
+function statusColor(status: AgentExecutionStatus): 'success' | 'danger' | 'warning' | 'text-base' {
+ if (status === 'success') return 'success';
+ if (status === 'error') return 'danger';
+ if (status === 'running') return 'text-base';
+ return 'warning';
+}
+
+function statusLabel(status: AgentExecutionStatus): string {
+ switch (status) {
+ case 'running':
+ return i18n.baseText('agentSessions.status.running');
+ case 'success':
+ return i18n.baseText('agentSessions.success');
+ case 'error':
+ return i18n.baseText('agentSessions.timeline.error');
+ case 'cancelled':
+ return i18n.baseText('agentSessions.status.cancelled');
+ case 'interrupted':
+ return i18n.baseText('agentSessions.status.interrupted');
+ }
+}
+
function originPresentation(thread: AgentExecutionThread): OriginPresentation {
const rawSource = thread.source?.trim();
const source = rawSource ? rawSource.toLowerCase() : undefined;
@@ -210,19 +235,42 @@ async function loadMore() {
-
+
|
|
@@ -238,9 +286,6 @@ async function loadMore() {
{{ (thread.totalPromptTokens + thread.totalCompletionTokens).toLocaleString() }}t
|
-
- {{ formatDuration(thread.totalDuration) }}
- |
|
-
- |
+ |
+ |
|
@@ -263,7 +308,7 @@ async function loadMore() {
v-if="!sessionsStore.loading && !sessionsStore.threads.length"
:class="$style.lastRow"
>
-
+ |
{{ i18n.baseText('agentSessions.empty') }}
@@ -272,7 +317,7 @@ async function loadMore() {
|
- |
+ |
|