diff --git a/src/app/services/blocks/blocks.ts b/src/app/services/blocks/blocks.ts
index 1a82608..7ca57ae 100644
--- a/src/app/services/blocks/blocks.ts
+++ b/src/app/services/blocks/blocks.ts
@@ -64,6 +64,10 @@ export class BlocksService {
return blockTypes.find((blockType) => blockType.type === typeName);
}
+ peekBlockType(typeName: BlockTypeName) {
+ return this._blockTypes().find((blockType) => blockType.type === typeName) ?? null;
+ }
+
createEmptyBlock(blockType: BlockTypeName, context?: BlockDraftContext) {
const flowId = typeof context?.flowId === 'string' && context.flowId.trim().length > 0 ? context.flowId.trim() : '';
const cacheKey = `${String(blockType)}::${flowId}`;
diff --git a/src/app/services/containers/containers.ts b/src/app/services/containers/containers.ts
index 60f18c9..7a384b2 100644
--- a/src/app/services/containers/containers.ts
+++ b/src/app/services/containers/containers.ts
@@ -64,6 +64,10 @@ export class ContainersService {
return containerTypes.find((containerType) => containerType.type === typeName);
}
+ peekContainerType(typeName: BlockTypeName) {
+ return this._containerTypes().find((containerType) => containerType.type === typeName) ?? null;
+ }
+
createEmptyContainer(containerType: BlockTypeName) {
const cacheKey = String(containerType);
const cached = this.emptyContainerCache.get(cacheKey);
diff --git a/src/app/shared/nodes/container-node/container-node.css b/src/app/shared/nodes/container-node/container-node.css
index 0427e18..dfe9a1b 100644
--- a/src/app/shared/nodes/container-node/container-node.css
+++ b/src/app/shared/nodes/container-node/container-node.css
@@ -423,6 +423,16 @@
white-space: nowrap;
}
+.container-node__param-value--clamped {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
.container-node__param-edit {
border: 0;
background: transparent;
@@ -445,6 +455,7 @@
pointer-events: none;
}
+
.container-node__dropzone {
margin: 0 14px 14px;
min-height: 128px;
diff --git a/src/app/shared/nodes/container-node/container-node.html b/src/app/shared/nodes/container-node/container-node.html
index 0e59983..7aec37f 100644
--- a/src/app/shared/nodes/container-node/container-node.html
+++ b/src/app/shared/nodes/container-node/container-node.html
@@ -140,7 +140,7 @@
}
- {{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}
+ {{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}
}
@for (contentField of richContentFields; track contentField.path) {
@@ -148,7 +148,7 @@
+
@for (part of contentField.parts; track $index) {
@if (part.isDynamicInput) {
{{ formatDynamicInputToken(part.text) }}
diff --git a/src/app/shared/nodes/container-node/container-node.ts b/src/app/shared/nodes/container-node/container-node.ts
index a616901..5eb30b9 100644
--- a/src/app/shared/nodes/container-node/container-node.ts
+++ b/src/app/shared/nodes/container-node/container-node.ts
@@ -37,6 +37,7 @@ type ContainerFieldView = {
label: string;
value: string;
wide: boolean;
+ expandable: boolean;
enabled: boolean;
type: ContainerFieldType;
booleanValue: boolean;
@@ -45,6 +46,8 @@ type ContainerFieldView = {
type RichContentView = {
path: string;
label: string;
+ rawValue: string;
+ expandable: boolean;
parts: { text: string; isDynamicInput: boolean }[];
};
@@ -569,7 +572,7 @@ export class ContainerNodeComponent {
private async loadSchemaContext() {
try {
- const containerType = await this.containersService.getContainerType(this.typeName);
+ const containerType = this.containersService.peekContainerType(this.typeName) ?? await this.containersService.getContainerType(this.typeName);
this.containerSchema = (containerType?.schema ?? null) as Record | null;
this.schemaRequirements = extractSchemaRequirements(this.containerSchema);
this.containerFieldDefinitions = this.buildContainerFieldDefinitions(this.containerSchema);
@@ -602,11 +605,16 @@ export class ContainerNodeComponent {
const richContentPaths = new Set(this.richContentPaths());
this.richContentFields = this.richContentPaths()
.filter((path) => this.isFieldVisible(path))
- .map((path) => ({
- path,
- label: this.containerFieldDefinitions.find((field) => field.path === path)?.label ?? pathToLabel(path),
- parts: this.toRichContentParts(path)
- }))
+ .map((path) => {
+ const rawValue = String(getValueByPath(config, path) ?? '');
+ return {
+ path,
+ label: this.containerFieldDefinitions.find((field) => field.path === path)?.label ?? pathToLabel(path),
+ rawValue,
+ expandable: this.isLongTextValue(rawValue),
+ parts: this.toRichContentParts(path)
+ };
+ })
.filter((field) => field.parts.length > 0);
this.parameterFields = this.containerFieldDefinitions
@@ -617,6 +625,7 @@ export class ContainerNodeComponent {
label: field.label,
value: valueToDisplayString(getValueByPath(config, field.path)),
wide: field.widget === 'textarea' || field.label.length >= 18,
+ expandable: this.isLongTextValue(valueToDisplayString(getValueByPath(config, field.path))),
enabled: this.isFieldEnabled(field.path),
type: field.type,
booleanValue: getValueByPath(config, field.path) === true
@@ -775,6 +784,10 @@ export class ContainerNodeComponent {
.filter((option: NodeSettingOption | null): option is NodeSettingOption => option != null);
}
+ private isLongTextValue(value: string): boolean {
+ return String(value ?? '').trim().length > 80;
+ }
+
private getEditorInitialValue(definition: ContainerFieldDefinition): string | boolean {
const raw = getValueByPath(this.configuration ?? {}, definition.path);
if (definition.type === 'boolean') return raw === true;
diff --git a/src/app/shared/nodes/generic-node/generic-node.css b/src/app/shared/nodes/generic-node/generic-node.css
index d07854d..c26d5e4 100644
--- a/src/app/shared/nodes/generic-node/generic-node.css
+++ b/src/app/shared/nodes/generic-node/generic-node.css
@@ -897,6 +897,14 @@
line-height: 1.45;
}
+.llm-param-text-clamped {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
.llm-inline-token {
display: inline-flex;
align-items: center;
@@ -1004,6 +1012,17 @@
white-space: nowrap;
}
+.llm-param-value-clamped {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+
.llm-modal-backdrop {
position: fixed;
inset: 0;
diff --git a/src/app/shared/nodes/generic-node/generic-node.html b/src/app/shared/nodes/generic-node/generic-node.html
index 0481618..0a62268 100644
--- a/src/app/shared/nodes/generic-node/generic-node.html
+++ b/src/app/shared/nodes/generic-node/generic-node.html
@@ -203,7 +203,7 @@
}
-
{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}
+
{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}
}
@@ -242,7 +242,7 @@
}
-
+
@if (!contentField.parts.length) {
-
} @else {
diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts
index cb34b94..db7be6e 100644
--- a/src/app/shared/nodes/generic-node/generic-node.ts
+++ b/src/app/shared/nodes/generic-node/generic-node.ts
@@ -86,6 +86,7 @@ type EditableFieldView = {
label: string;
value: string;
wide: boolean;
+ expandable: boolean;
enabled: boolean;
type: FieldType;
booleanValue: boolean;
@@ -124,6 +125,8 @@ type EditableFieldGroupView = {
type RichContentView = {
path: string;
label: string;
+ rawValue: string;
+ expandable: boolean;
parts: { text: string; isDynamicInput: boolean }[];
};
@@ -545,7 +548,7 @@ export class GenericNodeComponent {
}
try {
- const blockType = await this.blocksService.getBlockType(type);
+ const blockType = this.blocksService.peekBlockType(type) ?? await this.blocksService.getBlockType(type);
this.blockDescriptor = blockType ?? null;
this.blockSchema = (blockType?.schema ?? null) as Record
| null;
this.schemaRequirements = extractSchemaRequirements(this.blockSchema);
@@ -1078,6 +1081,10 @@ export class GenericNodeComponent {
.filter((option): option is NodeSettingOption => option != null);
}
+ private isLongTextValue(value: string): boolean {
+ return String(value ?? '').trim().length > 80;
+ }
+
private refreshParameterFields() {
const config = this.blockConfiguration ?? {};
const richContentPaths = new Set(this.richContentPaths());
@@ -1086,11 +1093,16 @@ export class GenericNodeComponent {
this.richContentFields = this.richContentPaths()
.filter((path) => this.isPathVisible(path))
- .map((path) => ({
- path,
- label: this.fieldDisplayLabel(path),
- parts: this.toRichContentParts(path)
- }));
+ .map((path) => {
+ const rawValue = String(this.getByPath(config, path) ?? '');
+ return {
+ path,
+ label: this.fieldDisplayLabel(path),
+ rawValue,
+ expandable: this.isLongTextValue(rawValue),
+ parts: this.toRichContentParts(path)
+ };
+ });
this.arrayFields = this.arrayFieldDefinitions
.filter((definition) => this.isPathVisible(definition.path))
@@ -1108,6 +1120,7 @@ export class GenericNodeComponent {
label: definition.label,
value: this.fieldDisplayValue(definition, value),
wide: this.shouldRenderWideField(definition.label, definition.ui.widget === 'textarea'),
+ expandable: this.isLongTextValue(this.fieldDisplayValue(definition, value)),
enabled: this.isPathEnabled(definition.path),
type: definition.type,
booleanValue: value === true
@@ -1145,6 +1158,7 @@ export class GenericNodeComponent {
label: this.fieldDisplayLabel(entry.path),
value: valueToDisplayString(entry.value),
wide: this.shouldRenderWideField(this.fieldDisplayLabel(entry.path), false),
+ expandable: this.isLongTextValue(valueToDisplayString(entry.value)),
enabled: this.isPathEnabled(entry.path),
type: (typeof entry.value === 'boolean' ? 'boolean' : 'unknown') as FieldType,
booleanValue: entry.value === true
diff --git a/src/app/shared/nodes/task-step-node/task-step-node.ts b/src/app/shared/nodes/task-step-node/task-step-node.ts
index 117c82f..bd71faf 100644
--- a/src/app/shared/nodes/task-step-node/task-step-node.ts
+++ b/src/app/shared/nodes/task-step-node/task-step-node.ts
@@ -543,9 +543,14 @@ export class TaskStepNodeComponent {
const nodeFamily = this.data?.data?.nodeFamily === 'container'
? 'container'
: 'block';
- const typeDescriptor = nodeFamily === 'container'
- ? await this.containersService.getContainerType(type)
- : await this.blocksService.getBlockType(type);
+ const cachedDescriptor = nodeFamily === 'container'
+ ? this.containersService.peekContainerType(type)
+ : this.blocksService.peekBlockType(type);
+ const typeDescriptor = cachedDescriptor ?? (
+ nodeFamily === 'container'
+ ? await this.containersService.getContainerType(type)
+ : await this.blocksService.getBlockType(type)
+ );
this.blockDescriptor = (typeDescriptor ?? null) as BlockType | null;
this.blockSchema = (typeDescriptor?.schema ?? null) as Record | null;
const typeKey = this.nodeTypeCacheKey();
@@ -1076,7 +1081,7 @@ export class TaskStepNodeComponent {
const normalized = String(value ?? '');
if (!normalized.trim()) return false;
const lineCount = normalized.split(/\r?\n/).length;
- return lineCount > 2 || normalized.length > 160;
+ return lineCount > 2 || normalized.length > 80;
}
private async openReadonlyTextDialog(label: string, value: string) {
diff --git a/src/app/shared/rete-editor/rete-editor.ts b/src/app/shared/rete-editor/rete-editor.ts
index 353a9b7..9a55c04 100644
--- a/src/app/shared/rete-editor/rete-editor.ts
+++ b/src/app/shared/rete-editor/rete-editor.ts
@@ -351,17 +351,49 @@ export class ReteEditor implements OnChanges, OnDestroy {
const currentNode = currentNodes.get(String(nextNode.id));
if (!currentNode?.data) continue;
- currentNode.data = {
+ const nextReadonlyNode = {
...currentNode.data,
...nextNode,
position: nextNode.position ?? currentNode.data.position,
__readonly: true
};
+ if (this.hasSameReadonlyNodePayload(currentNode.data as FlowNode & Record, nextReadonlyNode as FlowNode & Record)) {
+ continue;
+ }
+
+ currentNode.data = nextReadonlyNode;
+
await rete.area.update('node', currentNode.id);
}
}
+ private hasSameReadonlyNodePayload(currentNode: FlowNode & Record, nextNode: FlowNode & Record) {
+ return JSON.stringify({
+ id: currentNode.id,
+ name: currentNode.name,
+ position: currentNode.position,
+ inputs: currentNode.inputs,
+ outputs: currentNode.outputs,
+ specificConfiguration: currentNode.specificConfiguration,
+ typeName: currentNode.typeName,
+ userInteractive: currentNode['userInteractive'],
+ nodeFamily: currentNode.nodeFamily,
+ __readonly: currentNode['__readonly']
+ }) === JSON.stringify({
+ id: nextNode.id,
+ name: nextNode.name,
+ position: nextNode.position,
+ inputs: nextNode.inputs,
+ outputs: nextNode.outputs,
+ specificConfiguration: nextNode.specificConfiguration,
+ typeName: nextNode.typeName,
+ userInteractive: nextNode['userInteractive'],
+ nodeFamily: nextNode.nodeFamily,
+ __readonly: nextNode['__readonly']
+ });
+ }
+
private getDropPosition(event: DragEvent) {
const host = this.container.nativeElement as HTMLElement;
const rect = host.getBoundingClientRect();
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 da1a0b3..2e27083 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.css
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.css
@@ -202,16 +202,8 @@
color: #991b1b;
}
-.execution-log-details {
- margin: 10px 0 0;
- border: 1px solid #dbe4ee;
- border-radius: 10px;
- background: #ffffff;
- color: #0f172a;
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- font-size: 11px;
- line-height: 1.45;
- padding: 10px;
- white-space: pre-wrap;
- word-break: break-word;
+.execution-logs-scroll {
+ height: 100%;
+ overflow-y: auto;
+ overscroll-behavior: contain;
}
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 ab2096b..ec7ff37 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.html
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.html
@@ -154,7 +154,7 @@
(fileInputChange)="onFileInputChange($event.input, $event.files)">
} @else if (activeAsideTab() === 'logs') {
-
+
- @if (event.details) {
-
{{ event.details | json }}
- }
}
}
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 d41af38..a0bc087 100644
--- a/src/app/shared/task-execution-viewer/task-execution-viewer.ts
+++ b/src/app/shared/task-execution-viewer/task-execution-viewer.ts
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
-import { Component, computed, effect, inject, input, OnDestroy, signal } from '@angular/core';
+import { Component, computed, effect, ElementRef, inject, input, OnDestroy, signal, ViewChild } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
@@ -84,6 +84,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
readonly executionLogs = signal([]);
readonly logsLoading = signal(false);
readonly logsError = signal(null);
+ @ViewChild('logsScrollViewport') private logsScrollViewport?: ElementRef;
constructor() {
effect(() => {
@@ -141,6 +142,12 @@ export class TaskExecutionViewerComponent implements OnDestroy {
onCleanup(() => clearInterval(timer));
});
+ effect(() => {
+ this.executionLogs();
+ this.logsLoading();
+ queueMicrotask(() => this.scrollLogsToBottom());
+ });
+
effect(() => {
const dialogState = this.humanInteractionDialog.state();
const execution = this.execution();
@@ -732,6 +739,12 @@ export class TaskExecutionViewerComponent implements OnDestroy {
});
}
+ private scrollLogsToBottom() {
+ const element = this.logsScrollViewport?.nativeElement;
+ if (!element || this.activeAsideTab() !== 'logs') return;
+ element.scrollTop = element.scrollHeight;
+ }
+
private async loadSimulationOptions(
key: string,
context: Record,