diff --git a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.html b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.html
index d660ba6..489cd3a 100644
--- a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.html
+++ b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.html
@@ -70,7 +70,10 @@
}
@for (executionInput of editableInputs(); track executionInput.key) {
-
{{ executionInput.label }}
+
+
+
{{ executionInput.subtitle }}
+
Type: {{ executionInput.type }}
@if (isFileInput(executionInput)) {
diff --git a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.ts b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.ts
index 9ba5a06..fe970c2 100644
--- a/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.ts
+++ b/src/app/shared/task-execution-inputs-panel/task-execution-inputs-panel.ts
@@ -7,7 +7,8 @@ export type EditableExecutionInput = {
key: string;
nodeId: string;
inputName: string;
- label: string;
+ title: string;
+ subtitle: string;
type: string;
value: string;
};
diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.css b/src/app/shared/task-execution-viewer/task-execution-viewer.css
index 780064d..f8c8b0c 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.css
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.css
@@ -54,3 +54,75 @@
white-space: pre-wrap;
word-break: break-word;
}
+
+.execution-aside-tab {
+ border: 0;
+ border-bottom: 2px solid transparent;
+ background: transparent;
+ color: #64748b;
+ font-size: 12px;
+ font-weight: 700;
+ padding: 9px 12px;
+ cursor: pointer;
+}
+
+.execution-aside-tab-active {
+ border-bottom-color: #2563eb;
+ color: #1d4ed8;
+}
+
+.execution-aside-tab:disabled {
+ cursor: not-allowed;
+ opacity: 0.45;
+}
+
+.execution-output-modal {
+ margin: 0;
+ border: 1px solid #e2e8f0;
+ border-radius: 10px;
+ background: #f8fafc;
+ color: #0f172a;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ font-size: 12px;
+ line-height: 1.45;
+ padding: 14px;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.execution-output-badge {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 52px;
+ border: 1px solid #bfdbfe;
+ border-radius: 10px;
+ background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
+ color: #1d4ed8;
+ line-height: 1.2;
+ padding: 8px 12px;
+ text-align: center;
+}
+
+.execution-output-title {
+ font-size: 14px;
+ font-weight: 800;
+}
+
+.execution-output-subtitle {
+ margin-top: 2px;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: #2563eb;
+}
+
+.execution-output-preview {
+ white-space: pre-wrap;
+ word-break: break-word;
+ color: #0f172a;
+ font-size: 12px;
+ line-height: 1.45;
+}
diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.html b/src/app/shared/task-execution-viewer/task-execution-viewer.html
index 605461c..3c90e5b 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.html
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.html
@@ -63,26 +63,88 @@
@if (contextAsideOpen()) {
-
+ @if (outputPreviewModal(); as outputModal) {
+
+
+
+
+
+
{{ outputModal.title }}
+
{{ outputModal.subtitle }}
+
+
+
+
+
{{ outputModal.value }}
+
+
+
+ }
} @else {
Select an execution from the left panel
diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.ts b/src/app/shared/task-execution-viewer/task-execution-viewer.ts
index 076dcbb..238c1b9 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.ts
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.ts
@@ -14,6 +14,15 @@ import {
import { ReteEditor } from '@shared/rete-editor/rete-editor';
import { TaskExecutionsService } from '@services/task-executions/task-executions';
+type ExecutionOutputEntry = {
+ key: string;
+ title: string;
+ subtitle: string;
+ value: string;
+ preview: string;
+ isLong: boolean;
+};
+
@Component({
selector: 'app-task-execution-viewer',
imports: [CommonModule, ReteEditor, TaskExecutionInputsPanelComponent],
@@ -27,6 +36,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
private lastExecutionId: string | null = null;
readonly execution = input(null);
readonly contextAsideOpen = signal(true);
+ readonly activeAsideTab = signal<'inputs' | 'output'>('inputs');
readonly startInProgress = signal(false);
readonly savingInputs = signal>({});
readonly savingErrors = signal>({});
@@ -34,6 +44,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
readonly pendingAuthorizationValues = signal>({});
readonly savingAuthorizations = signal>({});
readonly authorizationErrors = signal>({});
+ readonly outputPreviewModal = signal(null);
constructor() {
effect(() => {
@@ -43,6 +54,14 @@ export class TaskExecutionViewerComponent implements OnDestroy {
this.pendingAuthorizationValues.set({});
this.savingAuthorizations.set({});
this.authorizationErrors.set({});
+ this.activeAsideTab.set('inputs');
+ this.outputPreviewModal.set(null);
+ });
+
+ effect(() => {
+ if (!this.executionOutputTabEnabled() && this.activeAsideTab() === 'output') {
+ this.activeAsideTab.set('inputs');
+ }
});
}
@@ -109,6 +128,35 @@ export class TaskExecutionViewerComponent implements OnDestroy {
return this.formatDuration(context.startTime, context.endTime);
});
+ readonly executionOutputTabEnabled = computed(() => {
+ const status = String(this.execution()?.context.status ?? '').toUpperCase();
+ return status === 'SUCCESS' || status === 'COMPLETED';
+ });
+
+ readonly executionOutputs = computed(() => {
+ const steps = this.execution()?.context.steps ?? {};
+ const resultMap = {
+ ...(this.execution()?.context.result ?? {}),
+ ...(this.execution()?.context.executionResult ?? {})
+ };
+
+ return Object.entries(resultMap)
+ .map(([key, rawValue]) => {
+ const value = this.stringifyOutputValue(rawValue);
+ const isLong = value.length > 160;
+ const label = this.formatExecutionOutputLabel(key, steps);
+ return {
+ key,
+ title: label.title,
+ subtitle: label.subtitle,
+ value,
+ preview: isLong ? `${value.slice(0, 160)}...` : value,
+ isLong
+ };
+ })
+ .sort((a, b) => a.title.localeCompare(b.title) || a.subtitle.localeCompare(b.subtitle));
+ });
+
readonly inputsReadOnly = computed(() => {
const status = this.execution()?.context.status;
return getExecutionStatusGroup(status) !== 'INIT';
@@ -168,20 +216,39 @@ export class TaskExecutionViewerComponent implements OnDestroy {
key,
nodeId: step.id,
inputName,
- label: `${step.block.name}.${inputName}`,
+ title: step.block.name,
+ subtitle: inputName,
type: String(input.descriptor?.type ?? 'TEXT').toUpperCase(),
value: pendingValue ?? (rawValue == null ? '' : String(rawValue))
});
}
}
- return entries.sort((a, b) => a.label.localeCompare(b.label));
+ return entries.sort((a, b) => a.title.localeCompare(b.title) || a.subtitle.localeCompare(b.subtitle));
});
toggleContextAside() {
this.contextAsideOpen.update((open) => !open);
}
+ selectAsideTab(tab: 'inputs' | 'output') {
+ if (tab === 'output' && !this.executionOutputTabEnabled()) return;
+ this.activeAsideTab.set(tab);
+ }
+
+ openOutputPreview(output: ExecutionOutputEntry, event?: Event) {
+ event?.preventDefault();
+ event?.stopPropagation();
+ if (!output.isLong) return;
+ this.outputPreviewModal.set(output);
+ }
+
+ closeOutputPreview(event?: Event) {
+ event?.preventDefault();
+ event?.stopPropagation();
+ this.outputPreviewModal.set(null);
+ }
+
startExecution() {
const executionId = this.execution()?.id;
if (!executionId || !this.canStartExecution() || this.startInProgress()) return;
@@ -326,6 +393,37 @@ export class TaskExecutionViewerComponent implements OnDestroy {
});
}
+ private stringifyOutputValue(value: unknown): string {
+ if (value == null) return '';
+ if (typeof value === 'string') return value;
+
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+ }
+
+ private formatExecutionOutputLabel(
+ key: string,
+ steps: Record
+ ): { title: string; subtitle: string } {
+ const separatorIndex = key.indexOf(':');
+ if (separatorIndex < 0) {
+ return { title: key, subtitle: 'Execution output' };
+ }
+
+ const nodeId = key.slice(0, separatorIndex);
+ const outputName = key.slice(separatorIndex + 1);
+ const step = steps[nodeId];
+ const blockName = step?.block?.name?.trim();
+
+ return {
+ title: blockName || key,
+ subtitle: outputName || 'Execution output'
+ };
+ }
+
private clearDebounceTimer(timerKey: string) {
const timer = this.textInputDebounceTimers.get(timerKey);
if (!timer) return;