Show JSON values as trees, and keep a read-only graph's connections unselectable
Outputs, intermediate inputs, loop rounds and outcome payloads that are JSON - objects, arrays, or JSON a model wrote as text, fenced or not - are shown with the JSON tree instead of their text. A preview opens the first level and a few entries, summing up the rest; the full view opens the whole tree. The JSON viewer gains the depth and entry limits this takes. In a read-only graph, such as an execution's, connections can no longer be picked or shown as selected: there is nothing to do with one there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
022df1909a
commit
3ffaf6e596
|
|
@ -20,6 +20,7 @@ import { BiasComparisonViewStateService } from '@services/bias/bias-comparison-v
|
|||
template: `
|
||||
<svg data-testid="connection" [attr.data-connection-id]="connectionId" [attr.data-loop-back]="isLoopBack || null">
|
||||
<path
|
||||
[class.connection-readonly]="isReadonly"
|
||||
[attr.d]="drawnPath"
|
||||
[attr.stroke]="strokeColor"
|
||||
[attr.stroke-width]="strokeWidth"
|
||||
|
|
@ -35,7 +36,8 @@ import { BiasComparisonViewStateService } from '@services/bias/bias-comparison-v
|
|||
[attr.y]="loopLabelY"
|
||||
width="190"
|
||||
height="34">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" class="loop-label" (pointerdown)="selectConnection($event)">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" class="loop-label" [class.loop-label-readonly]="isReadonly"
|
||||
(pointerdown)="selectConnection($event)">
|
||||
<span class="loop-glyph" aria-hidden="true">↻</span>
|
||||
@if (isSelected && !isReadonly) {
|
||||
<label class="loop-limit">
|
||||
|
|
@ -101,6 +103,16 @@ import { BiasComparisonViewStateService } from '@services/bias/bias-comparison-v
|
|||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Read-only graphs - an execution's - show connections, they do not let anyone pick one. */
|
||||
:host path.connection-readonly {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:host .loop-label.loop-label-readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:host .connection-delete {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -252,7 +264,7 @@ export class CustomConnectionComponent {
|
|||
}
|
||||
|
||||
get isSelected(): boolean {
|
||||
return this.graphSelection.selectedConnectionId() === this.connectionId;
|
||||
return !this.isReadonly && this.graphSelection.selectedConnectionId() === this.connectionId;
|
||||
}
|
||||
|
||||
get isBiasRoutingChange(): boolean {
|
||||
|
|
@ -293,6 +305,7 @@ export class CustomConnectionComponent {
|
|||
}
|
||||
|
||||
selectConnection(event: Event) {
|
||||
if (this.isReadonly) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.graphSelection.selectConnection(this.connectionId);
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@
|
|||
.json-viewer__entry { align-items: flex-start; display: flex; gap: .45rem; line-height: 1.55; }
|
||||
.json-viewer__key { color: #7c3aed; white-space: nowrap; }
|
||||
.json-viewer__primitive { color: #0f172a; flex: 1 1 auto; font-family: inherit; min-width: 0; white-space: pre-wrap; word-break: break-word; }
|
||||
.json-viewer__more { color: #64748b; font-style: italic; line-height: 1.55; }
|
||||
|
|
|
|||
|
|
@ -9,26 +9,29 @@
|
|||
<div class="json-viewer__label">{{ label }}</div>
|
||||
}
|
||||
|
||||
<ng-template #renderValue let-value let-path="path">
|
||||
<ng-template #renderValue let-value let-path="path" let-depth="depth">
|
||||
@if (isExpandable(value)) {
|
||||
<details class="json-viewer__group" [open]="isExpanded(path)" (toggle)="onToggle(path, $event)">
|
||||
<details class="json-viewer__group" [open]="isExpanded(path, depth)" (toggle)="onToggle(path, $event)">
|
||||
<summary>{{ summary(value) }}</summary>
|
||||
<div class="json-viewer__children">
|
||||
@if (isArray(value)) {
|
||||
@for (item of value; track $index) {
|
||||
@for (item of items(value); track $index) {
|
||||
<div class="json-viewer__entry">
|
||||
<span class="json-viewer__key">[{{ $index }}]</span>
|
||||
<ng-container *ngTemplateOutlet="renderValue; context: { $implicit: item, path: path + '[' + $index + ']' }" />
|
||||
<ng-container *ngTemplateOutlet="renderValue; context: { $implicit: item, path: path + '[' + $index + ']', depth: depth + 1 }" />
|
||||
</div>
|
||||
}
|
||||
} @else if (isObject(value)) {
|
||||
@for (entry of entries(value); track entry.key) {
|
||||
<div class="json-viewer__entry">
|
||||
<span class="json-viewer__key">{{ entry.key }}</span>
|
||||
<ng-container *ngTemplateOutlet="renderValue; context: { $implicit: entry.value, path: path + '.' + entry.key }" />
|
||||
<ng-container *ngTemplateOutlet="renderValue; context: { $implicit: entry.value, path: path + '.' + entry.key, depth: depth + 1 }" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (hiddenCount(value); as hidden) {
|
||||
<div class="json-viewer__more">… {{ hidden }} more</div>
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
} @else {
|
||||
|
|
@ -36,5 +39,5 @@
|
|||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngTemplateOutlet="renderValue; context: { $implicit: value, path: '$' }" />
|
||||
<ng-container *ngTemplateOutlet="renderValue; context: { $implicit: value, path: '$', depth: 0 }" />
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,18 @@ describe('JsonViewerComponent', () => {
|
|||
expect(text).toContain('[0]');
|
||||
});
|
||||
|
||||
it('as a preview opens only the first level and sums up what it leaves out', () => {
|
||||
fixture.componentInstance.value = { a: 1, b: 2, c: { deep: 'hidden' }, d: 4 };
|
||||
fixture.componentInstance.expandDepth = 1;
|
||||
fixture.componentInstance.maxEntries = 3;
|
||||
fixture.detectChanges();
|
||||
|
||||
const groups = [...fixture.nativeElement.querySelectorAll('details')] as HTMLDetailsElement[];
|
||||
expect(groups.map((group) => group.open)).toEqual([true, false]);
|
||||
expect(fixture.nativeElement.textContent).toContain('… 1 more');
|
||||
expect(fixture.nativeElement.textContent).not.toContain('"d"');
|
||||
});
|
||||
|
||||
it('formats primitive strings as JSON strings', () => {
|
||||
expect(fixture.componentInstance.formatPrimitive('hello')).toBe('"hello"');
|
||||
expect(fixture.componentInstance.formatPrimitive(null)).toBe('null');
|
||||
|
|
|
|||
|
|
@ -18,8 +18,13 @@ export class JsonViewerComponent {
|
|||
@Input() value: unknown;
|
||||
@Input() label: string | null = null;
|
||||
@Input() initiallyExpanded = true;
|
||||
/** How many levels start open; deeper ones start closed. The root is level 0. */
|
||||
@Input() expandDepth = Number.POSITIVE_INFINITY;
|
||||
/** At most this many entries per object or array, the rest summed up as "N more". */
|
||||
@Input() maxEntries = Number.POSITIVE_INFINITY;
|
||||
|
||||
private readonly collapsedPaths = new Set<string>();
|
||||
/** What the person opened or closed by hand, which wins over where it started. */
|
||||
private readonly toggledPaths = new Map<string, boolean>();
|
||||
|
||||
isArray(value: unknown): value is unknown[] {
|
||||
return Array.isArray(value);
|
||||
|
|
@ -34,7 +39,17 @@ export class JsonViewerComponent {
|
|||
}
|
||||
|
||||
entries(value: Record<string, unknown>): Array<{ key: string; value: unknown }> {
|
||||
return Object.entries(value).map(([key, entryValue]) => ({ key, value: entryValue }));
|
||||
return Object.entries(value).slice(0, this.maxEntries).map(([key, entryValue]) => ({ key, value: entryValue }));
|
||||
}
|
||||
|
||||
items(value: unknown[]): unknown[] {
|
||||
return value.slice(0, this.maxEntries);
|
||||
}
|
||||
|
||||
/** Entries left out by maxEntries, for the "N more" line. */
|
||||
hiddenCount(value: unknown): number {
|
||||
const size = this.isArray(value) ? value.length : this.isObject(value) ? Object.keys(value).length : 0;
|
||||
return Math.max(0, size - this.maxEntries);
|
||||
}
|
||||
|
||||
summary(value: unknown): string {
|
||||
|
|
@ -54,13 +69,11 @@ export class JsonViewerComponent {
|
|||
}
|
||||
}
|
||||
|
||||
isExpanded(path: string): boolean {
|
||||
return this.initiallyExpanded && !this.collapsedPaths.has(path);
|
||||
isExpanded(path: string, depth: number): boolean {
|
||||
return this.toggledPaths.get(path) ?? (this.initiallyExpanded && depth < this.expandDepth);
|
||||
}
|
||||
|
||||
onToggle(path: string, event: Event) {
|
||||
const expanded = (event.target as HTMLDetailsElement).open;
|
||||
if (expanded) this.collapsedPaths.delete(path);
|
||||
else this.collapsedPaths.add(path);
|
||||
this.toggledPaths.set(path, (event.target as HTMLDetailsElement).open);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
import { StepStatus, TaskExecution, TaskExecutionLoop, TaskExecutionStep } from '@models/task-execution';
|
||||
|
||||
import {
|
||||
asJsonTree,
|
||||
ExecutionIntermediateInputEntry,
|
||||
getExecutionInputValues,
|
||||
jsonPreviewIsPartial,
|
||||
stepTitle,
|
||||
stringifyOutputValue
|
||||
} from './execution-viewer.utils';
|
||||
|
|
@ -167,7 +169,8 @@ function inputEntries(keyPrefix: string, nodeTitle: string, values: Array<[strin
|
|||
const items = Array.isArray(rawValue) ? rawValue : [rawValue];
|
||||
return items.map((item, index) => {
|
||||
const value = stringifyOutputValue(item);
|
||||
const isLong = value.length > ROUND_INPUT_PREVIEW_LIMIT;
|
||||
const json = asJsonTree(item);
|
||||
const isLong = json ? jsonPreviewIsPartial(json) : value.length > ROUND_INPUT_PREVIEW_LIMIT;
|
||||
return {
|
||||
key: Array.isArray(rawValue) ? `${keyPrefix}:${inputName}:${index}` : `${keyPrefix}:${inputName}`,
|
||||
nodeTitle,
|
||||
|
|
@ -175,9 +178,10 @@ function inputEntries(keyPrefix: string, nodeTitle: string, values: Array<[strin
|
|||
inputType: '',
|
||||
sourceLabel: '',
|
||||
value,
|
||||
preview: isLong ? `${value.slice(0, ROUND_INPUT_PREVIEW_LIMIT)}...` : value,
|
||||
preview: value.length > ROUND_INPUT_PREVIEW_LIMIT ? `${value.slice(0, ROUND_INPUT_PREVIEW_LIMIT)}...` : value,
|
||||
isLong,
|
||||
itemLabel: Array.isArray(rawValue) ? `Item ${index + 1}` : null
|
||||
itemLabel: Array.isArray(rawValue) ? `Item ${index + 1}` : null,
|
||||
json
|
||||
};
|
||||
});
|
||||
}).filter((entry) => entry.value.trim().length > 0);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { TaskExecution, TaskExecutionStep } from '@models/task-execution';
|
||||
import {
|
||||
asJsonTree,
|
||||
buildAuthorizationGate,
|
||||
buildVisibleExecutionLogs,
|
||||
executionEventTitle,
|
||||
|
|
@ -14,6 +15,7 @@ import {
|
|||
inheritedSimulator,
|
||||
inheritedSimulatorNotice,
|
||||
isExecutionStartable,
|
||||
jsonPreviewIsPartial,
|
||||
planInputSaves,
|
||||
preparedInputValue,
|
||||
readSimulatorParameters
|
||||
|
|
@ -450,3 +452,26 @@ describe('inheritedSimulator', () => {
|
|||
expect(inheritedSimulatorNotice(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('asJsonTree', () => {
|
||||
it('takes objects and arrays, and JSON a model wrote as text, fenced or not', () => {
|
||||
expect(asJsonTree({ a: 1 })).toEqual({ a: 1 });
|
||||
expect(asJsonTree([1, 2])).toEqual([1, 2]);
|
||||
expect(asJsonTree(' {"verdict": "pass"} ')).toEqual({ verdict: 'pass' });
|
||||
expect(asJsonTree('```json\n[{"id": 1}]\n```')).toEqual([{ id: 1 }]);
|
||||
});
|
||||
|
||||
it('leaves prose, bare values and broken JSON as text', () => {
|
||||
expect(asJsonTree('Where every rhythm finds its perfect harmony')).toBeNull();
|
||||
expect(asJsonTree('"quoted"')).toBeNull();
|
||||
expect(asJsonTree('42')).toBeNull();
|
||||
expect(asJsonTree('{not json}')).toBeNull();
|
||||
expect(asJsonTree(null)).toBeNull();
|
||||
});
|
||||
|
||||
it('calls a preview partial when it hides a level or entries past the limit', () => {
|
||||
expect(jsonPreviewIsPartial({ a: 1, b: 2 })).toBe(false);
|
||||
expect(jsonPreviewIsPartial({ a: { b: 1 } })).toBe(true);
|
||||
expect(jsonPreviewIsPartial([1, 2, 3, 4, 5, 6])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export type ExecutionOutputEntry = {
|
|||
preview: string;
|
||||
isLong: boolean;
|
||||
itemLabel: string | null;
|
||||
/** The value as a JSON tree when it is one, for a tree view instead of its text. */
|
||||
json: unknown | null;
|
||||
};
|
||||
|
||||
export type ExecutionIntermediateInputEntry = {
|
||||
|
|
@ -44,6 +46,7 @@ export type ExecutionIntermediateInputEntry = {
|
|||
preview: string;
|
||||
isLong: boolean;
|
||||
itemLabel: string | null;
|
||||
json: unknown | null;
|
||||
};
|
||||
|
||||
export type ExecutionOutputGroup = {
|
||||
|
|
@ -79,6 +82,38 @@ export function stepNodeId(step: TaskExecutionStep | null | undefined): string {
|
|||
return getTaskExecutionStepNode(step)?.id || step?.id || '';
|
||||
}
|
||||
|
||||
/** Top-level entries a JSON preview shows before "N more"; the whole tree opens in the full view. */
|
||||
export const JSON_PREVIEW_ENTRIES = 5;
|
||||
|
||||
/**
|
||||
* The value as a JSON object or array, or null when it is not one. Models often answer with JSON
|
||||
* as text, and often inside a ```json fence, so a string holding one counts as well. A bare number
|
||||
* or quoted string is left as text: as a tree it would be a single leaf, no easier to read.
|
||||
*/
|
||||
export function asJsonTree(value: unknown): unknown | null {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value !== null && typeof value === 'object') return value;
|
||||
if (typeof value !== 'string') return null;
|
||||
let text = value.trim();
|
||||
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(text);
|
||||
if (fenced) text = fenced[1].trim();
|
||||
const looksLikeJson = (text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']'));
|
||||
if (!looksLikeJson) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
return parsed !== null && typeof parsed === 'object' ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a JSON preview leaves something out - a nested level, or entries past the limit. */
|
||||
export function jsonPreviewIsPartial(tree: unknown): boolean {
|
||||
const children = Array.isArray(tree) ? tree : tree !== null && typeof tree === 'object' ? Object.values(tree) : [];
|
||||
return children.length > JSON_PREVIEW_ENTRIES
|
||||
|| children.some((child) => child !== null && typeof child === 'object');
|
||||
}
|
||||
|
||||
export function stringifyOutputValue(value: unknown): string {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
|
|
@ -164,16 +199,18 @@ export function buildExecutionOutputs(execution: TaskExecution | null): Executio
|
|||
const values = Array.isArray(rawValue) ? rawValue : [rawValue];
|
||||
return values.map((item, index) => {
|
||||
const value = stringifyOutputValue(item);
|
||||
const isLong = value.length > OUTPUT_PREVIEW_LIMIT;
|
||||
const json = asJsonTree(item);
|
||||
const isLong = json ? jsonPreviewIsPartial(json) : value.length > OUTPUT_PREVIEW_LIMIT;
|
||||
const isArrayItem = Array.isArray(rawValue);
|
||||
return {
|
||||
key: isArrayItem ? `${key}:${index}` : key,
|
||||
nodeTitle: label.nodeTitle,
|
||||
outputName: label.outputName,
|
||||
value,
|
||||
preview: isLong ? `${value.slice(0, OUTPUT_PREVIEW_LIMIT)}...` : value,
|
||||
preview: value.length > OUTPUT_PREVIEW_LIMIT ? `${value.slice(0, OUTPUT_PREVIEW_LIMIT)}...` : value,
|
||||
isLong,
|
||||
itemLabel: isArrayItem ? `Item ${index + 1}` : null,
|
||||
json,
|
||||
};
|
||||
});
|
||||
})
|
||||
|
|
@ -205,7 +242,8 @@ export function buildExecutionIntermediateInputs(execution: TaskExecution | null
|
|||
|
||||
return values.map((item, index) => {
|
||||
const value = stringifyOutputValue(item);
|
||||
const isLong = value.length > INTERMEDIATE_INPUT_PREVIEW_LIMIT;
|
||||
const json = asJsonTree(item);
|
||||
const isLong = json ? jsonPreviewIsPartial(json) : value.length > INTERMEDIATE_INPUT_PREVIEW_LIMIT;
|
||||
return {
|
||||
key: isArrayValue ? `${step.id}:${inputName}:${index}` : `${step.id}:${inputName}`,
|
||||
nodeTitle: stepTitle(step),
|
||||
|
|
@ -213,9 +251,12 @@ export function buildExecutionIntermediateInputs(execution: TaskExecution | null
|
|||
inputType: String(input?.descriptor?.type ?? 'TEXT').toUpperCase(),
|
||||
sourceLabel: input?.registered ? 'Connected input' : 'Prepared input',
|
||||
value,
|
||||
preview: isLong ? `${value.slice(0, INTERMEDIATE_INPUT_PREVIEW_LIMIT)}...` : value,
|
||||
preview: value.length > INTERMEDIATE_INPUT_PREVIEW_LIMIT
|
||||
? `${value.slice(0, INTERMEDIATE_INPUT_PREVIEW_LIMIT)}...`
|
||||
: value,
|
||||
isLong,
|
||||
itemLabel: isArrayValue ? `Item ${index + 1}` : null,
|
||||
json,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -957,3 +957,14 @@
|
|||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* A JSON value in a preview: the first level, a few entries - the whole tree opens with the eye. */
|
||||
.execution-json-preview {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
max-height: 14rem;
|
||||
overflow: auto;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,9 @@
|
|||
|
||||
@if (outcome.payload !== null && outcome.payload !== undefined) {
|
||||
<div class="execution-outcome-payload">
|
||||
@if (isTextPayload(outcome.payload)) {
|
||||
@if (payloadJson(outcome.payload); as tree) {
|
||||
<app-json-viewer [value]="tree" [initiallyExpanded]="true" />
|
||||
} @else if (isTextPayload(outcome.payload)) {
|
||||
<div class="execution-outcome-text">{{ outcome.payload }}</div>
|
||||
} @else {
|
||||
<app-json-viewer [value]="outcome.payload" [initiallyExpanded]="true" />
|
||||
|
|
@ -443,7 +445,11 @@
|
|||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (input.json) {
|
||||
<app-json-viewer class="execution-json-preview" [value]="input.json" [expandDepth]="1" [maxEntries]="jsonPreviewEntries" />
|
||||
} @else {
|
||||
<div class="execution-intermediate-preview">{{ input.preview }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
@ -495,7 +501,11 @@
|
|||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="execution-intermediate-preview">{{ input.preview }}</div>
|
||||
@if (input.json) {
|
||||
<app-json-viewer class="execution-json-preview" [value]="input.json" [expandDepth]="1" [maxEntries]="jsonPreviewEntries" />
|
||||
} @else {
|
||||
<div class="execution-intermediate-preview">{{ input.preview }}</div>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="text-[11px] text-slate-500">No inputs recorded.</div>
|
||||
|
|
@ -614,7 +624,11 @@
|
|||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (output.json) {
|
||||
<app-json-viewer class="execution-json-preview" [value]="output.json" [expandDepth]="1" [maxEntries]="jsonPreviewEntries" />
|
||||
} @else {
|
||||
<div class="execution-output-preview">{{ output.preview }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
@ -648,7 +662,11 @@
|
|||
<button type="button" mat-stroked-button (click)="closeOutputPreview($event)">Close</button>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto px-5 py-4">
|
||||
@if (outputModal.json) {
|
||||
<app-json-viewer [value]="outputModal.json" />
|
||||
} @else {
|
||||
<pre class="execution-output-modal">{{ outputModal.value }}</pre>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -665,7 +683,11 @@
|
|||
<button type="button" mat-stroked-button (click)="closeIntermediateInputPreview($event)">Close</button>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto px-5 py-4">
|
||||
@if (inputModal.json) {
|
||||
<app-json-viewer [value]="inputModal.json" />
|
||||
} @else {
|
||||
<pre class="execution-output-modal">{{ inputModal.value }}</pre>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -108,6 +108,8 @@ import {
|
|||
VaultAuthorizationEntry,
|
||||
buildAuthorizationGate,
|
||||
isExecutionStartable,
|
||||
asJsonTree,
|
||||
JSON_PREVIEW_ENTRIES
|
||||
} from './execution-viewer.utils';
|
||||
import { buildExecutionLoopHistories, ExecutionLoopHistory } from './execution-rounds';
|
||||
import {
|
||||
|
|
@ -693,6 +695,13 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
return typeof payload === 'string';
|
||||
}
|
||||
|
||||
/** A payload that is JSON, including JSON a model wrote as text, as a tree; null otherwise. */
|
||||
payloadJson(payload: unknown): unknown | null {
|
||||
return typeof payload === 'string' ? asJsonTree(payload) : null;
|
||||
}
|
||||
|
||||
readonly jsonPreviewEntries = JSON_PREVIEW_ENTRIES;
|
||||
|
||||
stepNameForOutcome(stepId: string): string {
|
||||
return this.execution()?.context.steps?.[stepId]?.node?.name ?? stepId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -776,7 +776,8 @@ export async function refreshLoopMarkers(
|
|||
.filter((c) => getGraphConnectionKind(c.sourceOutput, c.targetInput) === 'dependency')
|
||||
.map((c) => ({ sourceId: c.source, targetId: c.target }))
|
||||
});
|
||||
for (const connection of data) {
|
||||
// Every connection, dependencies included: in a read-only graph none of them can be selected.
|
||||
for (const connection of connections) {
|
||||
const loopBack = backEdges.has(connection.id);
|
||||
const readonly = resolvedRuntime?.readonly === true;
|
||||
const top = loopBack ? loopTop(area, connection) : undefined;
|
||||
|
|
|
|||
Loading…
Reference in New Issue