@object-ui/app-shell 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/dist/console/AppContent.js +14 -2
  3. package/dist/console/ai/AiChatPage.js +11 -7
  4. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  5. package/dist/console/ai/LiveCanvas.js +6 -4
  6. package/dist/hooks/useChatConversation.d.ts +30 -0
  7. package/dist/hooks/useChatConversation.js +63 -0
  8. package/dist/hooks/useConsoleActionRuntime.js +6 -2
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +5 -1
  11. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  12. package/dist/layout/ConsoleFloatingChatbot.js +25 -8
  13. package/dist/layout/ContextSelectors.js +59 -35
  14. package/dist/layout/agentPicker.d.ts +56 -0
  15. package/dist/layout/agentPicker.js +40 -0
  16. package/dist/preview/CommitTimeline.d.ts +15 -0
  17. package/dist/preview/CommitTimeline.js +82 -0
  18. package/dist/preview/UnpublishedAppBar.js +11 -7
  19. package/dist/preview/commitHistory.d.ts +28 -0
  20. package/dist/preview/commitHistory.js +48 -0
  21. package/dist/providers/MetadataProvider.js +9 -0
  22. package/dist/views/FlowRunner.d.ts +2 -30
  23. package/dist/views/FlowRunner.js +18 -50
  24. package/dist/views/ScreenView.d.ts +70 -0
  25. package/dist/views/ScreenView.js +73 -0
  26. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  27. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  28. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  29. package/dist/views/metadata-admin/PackagesPage.js +9 -1
  30. package/dist/views/metadata-admin/ResourceEditPage.js +47 -20
  31. package/dist/views/metadata-admin/ResourceListPage.js +8 -16
  32. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  33. package/dist/views/metadata-admin/anchors.js +20 -2
  34. package/dist/views/metadata-admin/i18n.js +88 -2
  35. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +2 -2
  36. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +122 -8
  37. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +84 -3
  38. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +67 -2
  39. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  40. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  41. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  42. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  43. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  44. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  45. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  46. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
  47. package/dist/views/metadata-admin/inspectors/flow-node-config.js +46 -1
  48. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  49. package/dist/views/metadata-admin/issuePath.js +65 -0
  50. package/dist/views/metadata-admin/package-scope.d.ts +26 -0
  51. package/dist/views/metadata-admin/package-scope.js +43 -0
  52. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  53. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +7 -1
  54. package/dist/views/metadata-admin/previews/FlowCanvas.js +104 -16
  55. package/dist/views/metadata-admin/previews/FlowPreview.js +31 -3
  56. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  57. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  58. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  59. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  60. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  61. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  62. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  63. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +21 -6
  64. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  65. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  66. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  67. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  68. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +11 -0
  69. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  70. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +72 -0
  71. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  72. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  73. package/package.json +38 -38
@@ -0,0 +1,43 @@
1
+ /**
2
+ * screen-spec — pure helpers that map a flow `screen` node's authored `config`
3
+ * onto the runtime `ScreenSpec` (the contract {@link ScreenView} renders), plus
4
+ * `{var}` interpolation for the title/description and `visibleWhen` field
5
+ * gating. Kept framework-free so {@link ScreenPreview} stays a thin component
6
+ * and these stay unit-testable.
7
+ */
8
+ import type { ScreenSpec } from '../../ScreenView';
9
+ /** Minimal node shape the preview needs (id + authored config). */
10
+ export interface ScreenPreviewNode {
11
+ id: string;
12
+ label?: string;
13
+ config?: Record<string, unknown>;
14
+ }
15
+ /**
16
+ * Interpolate `{var}` references, mirroring the simulator's `{var}` syntax
17
+ * (flow-simulator.ts). Known vars are substituted; unknown refs stay literal so
18
+ * the author still sees the dependency in the design preview.
19
+ */
20
+ export declare function interpolate(text: string | undefined, vars: Record<string, unknown> | undefined): string;
21
+ /**
22
+ * A field's `visibleWhen` gate, evaluated against the current variables using
23
+ * the SAME evaluator the simulator uses for edge conditions (so the preview
24
+ * agrees with the simulator). Real metadata mixes `{var}` and bare-var styles
25
+ * (e.g. `{createOpportunity} == true`, `stage == "review"`), so brace
26
+ * placeholders are normalised to bare identifiers first.
27
+ *
28
+ * Fail-OPEN: a missing condition, an unparseable one, or one that references a
29
+ * not-yet-set variable (the inspector has no run state) keeps the field
30
+ * visible — the design preview never hides a configured field just because it
31
+ * lacks the data to decide. With live run state (the simulator) it gates
32
+ * faithfully: `createOpportunity == false` hides the field.
33
+ */
34
+ export declare function isFieldVisibleWhen(visibleWhen: unknown, variables: Record<string, unknown> | undefined): boolean;
35
+ /** Count of authored field rows hidden by their `visibleWhen` against `variables`. */
36
+ export declare function hiddenFieldCount(node: ScreenPreviewNode, variables: Record<string, unknown> | undefined): number;
37
+ /**
38
+ * Map a screen node's authored `config` onto the runtime `ScreenSpec` — the
39
+ * same keys the engine's `screen` executor reads (title / description / fields
40
+ * / objectName / mode / defaults / idVariable). `fields` are gated by their
41
+ * `visibleWhen` against `variables` (omit `variables` to keep every field).
42
+ */
43
+ export declare function buildScreenSpec(node: ScreenPreviewNode, variables?: Record<string, unknown>): ScreenSpec;
@@ -0,0 +1,108 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ import { evalCondition } from './simulator/flow-sim-validate';
3
+ /**
4
+ * Interpolate `{var}` references, mirroring the simulator's `{var}` syntax
5
+ * (flow-simulator.ts). Known vars are substituted; unknown refs stay literal so
6
+ * the author still sees the dependency in the design preview.
7
+ */
8
+ export function interpolate(text, vars) {
9
+ if (!text)
10
+ return '';
11
+ if (!vars)
12
+ return text;
13
+ return text.replace(/\{([^{}]+)\}/g, (m, k) => {
14
+ const v = vars[String(k).trim()];
15
+ return v === undefined || v === null ? m : String(v);
16
+ });
17
+ }
18
+ /**
19
+ * A field's `visibleWhen` gate, evaluated against the current variables using
20
+ * the SAME evaluator the simulator uses for edge conditions (so the preview
21
+ * agrees with the simulator). Real metadata mixes `{var}` and bare-var styles
22
+ * (e.g. `{createOpportunity} == true`, `stage == "review"`), so brace
23
+ * placeholders are normalised to bare identifiers first.
24
+ *
25
+ * Fail-OPEN: a missing condition, an unparseable one, or one that references a
26
+ * not-yet-set variable (the inspector has no run state) keeps the field
27
+ * visible — the design preview never hides a configured field just because it
28
+ * lacks the data to decide. With live run state (the simulator) it gates
29
+ * faithfully: `createOpportunity == false` hides the field.
30
+ */
31
+ export function isFieldVisibleWhen(visibleWhen, variables) {
32
+ if (typeof visibleWhen !== 'string' || !visibleWhen.trim())
33
+ return true;
34
+ if (!variables)
35
+ return true;
36
+ const normalized = visibleWhen.replace(/\{([\w.]+)\}/g, '$1');
37
+ const { result, error } = evalCondition(normalized, variables);
38
+ return error ? true : result;
39
+ }
40
+ /**
41
+ * Coerce the authored `config.fields` rows into runtime `ScreenFieldSpec`s,
42
+ * dropping any whose `visibleWhen` evaluates false against `variables` — exactly
43
+ * what the runtime `screen` executor emits (it filters server-side before
44
+ * sending the ScreenSpec).
45
+ */
46
+ function toScreenFields(raw, variables) {
47
+ if (!Array.isArray(raw))
48
+ return [];
49
+ const out = [];
50
+ for (const f of raw) {
51
+ if (!f || typeof f !== 'object')
52
+ continue;
53
+ const row = f;
54
+ if (typeof row.name !== 'string' || !row.name)
55
+ continue;
56
+ if (!isFieldVisibleWhen(row.visibleWhen, variables))
57
+ continue;
58
+ out.push({
59
+ name: row.name,
60
+ label: typeof row.label === 'string' ? row.label : undefined,
61
+ type: typeof row.type === 'string' ? row.type : undefined,
62
+ required: row.required === true,
63
+ });
64
+ }
65
+ return out;
66
+ }
67
+ /** Count of authored field rows hidden by their `visibleWhen` against `variables`. */
68
+ export function hiddenFieldCount(node, variables) {
69
+ const raw = node.config?.fields;
70
+ if (!Array.isArray(raw))
71
+ return 0;
72
+ let hidden = 0;
73
+ for (const f of raw) {
74
+ if (!f || typeof f !== 'object')
75
+ continue;
76
+ const row = f;
77
+ if (typeof row.name !== 'string' || !row.name)
78
+ continue;
79
+ if (!isFieldVisibleWhen(row.visibleWhen, variables))
80
+ hidden++;
81
+ }
82
+ return hidden;
83
+ }
84
+ /**
85
+ * Map a screen node's authored `config` onto the runtime `ScreenSpec` — the
86
+ * same keys the engine's `screen` executor reads (title / description / fields
87
+ * / objectName / mode / defaults / idVariable). `fields` are gated by their
88
+ * `visibleWhen` against `variables` (omit `variables` to keep every field).
89
+ */
90
+ export function buildScreenSpec(node, variables) {
91
+ const c = (node.config && typeof node.config === 'object' ? node.config : {});
92
+ const objectName = typeof c.objectName === 'string' && c.objectName ? c.objectName : undefined;
93
+ const mode = c.mode === 'edit' ? 'edit' : c.mode === 'create' ? 'create' : undefined;
94
+ const defaults = c.defaults && typeof c.defaults === 'object' && !Array.isArray(c.defaults)
95
+ ? c.defaults
96
+ : undefined;
97
+ return {
98
+ nodeId: node.id,
99
+ title: typeof c.title === 'string' ? c.title : undefined,
100
+ description: typeof c.description === 'string' ? c.description : undefined,
101
+ fields: toScreenFields(c.fields, variables),
102
+ kind: objectName ? 'object-form' : 'fields',
103
+ objectName,
104
+ mode,
105
+ defaults,
106
+ idVariable: typeof c.idVariable === 'string' ? c.idVariable : undefined,
107
+ };
108
+ }
@@ -28,6 +28,12 @@ export interface SimEdge {
28
28
  };
29
29
  isDefault?: boolean;
30
30
  label?: string;
31
+ /**
32
+ * Connection type (FlowEdgeSchema). A `'back'` edge is an ADR-0044 declared
33
+ * back-edge: traversed normally at run time, but excluded from DAG cycle
34
+ * validation so a revise/rework loop can re-enter an earlier node.
35
+ */
36
+ type?: string;
31
37
  }
32
38
  export type SimStatus = 'idle' | 'running' | 'paused' | 'done' | 'error';
33
39
  /** One evaluated outgoing edge of a decision (kept for the debug timeline). */
@@ -76,6 +82,11 @@ export interface Diagnostic {
76
82
  level: DiagnosticLevel;
77
83
  nodeId?: string;
78
84
  message: string;
85
+ /**
86
+ * For a cycle error: the node path that closes the loop (e.g. `['a','b','a']`),
87
+ * so the designer can paint the offending edges/nodes inline on the canvas.
88
+ */
89
+ cycle?: string[];
79
90
  }
80
91
  export interface FlowValidation {
81
92
  errors: Diagnostic[];
@@ -4,5 +4,12 @@ export declare function evalCondition(expr: string, variables: Record<string, un
4
4
  result: boolean;
5
5
  error?: string;
6
6
  };
7
+ /**
8
+ * Find a directed cycle in `edges` over `nodeIds`, returned as the node path
9
+ * that closes the loop (e.g. `['a','b','a']`), or `null` when the graph is a
10
+ * DAG. Iterative DFS with a recursion-stack colour map; the first cycle found
11
+ * wins (enough to report — the author fixes one at a time).
12
+ */
13
+ export declare function findCycle(nodeIds: string[], edges: SimEdge[]): string[] | null;
7
14
  /** Static structural checks; `errors` block Run, `warnings` are advisory. */
8
15
  export declare function validateFlowDraft(nodes: SimNode[], edges: SimEdge[]): FlowValidation;
@@ -28,6 +28,63 @@ export function evalCondition(expr, variables) {
28
28
  return { result: false, error: err.message || 'Evaluation failed.' };
29
29
  }
30
30
  }
31
+ /**
32
+ * Find a directed cycle in `edges` over `nodeIds`, returned as the node path
33
+ * that closes the loop (e.g. `['a','b','a']`), or `null` when the graph is a
34
+ * DAG. Iterative DFS with a recursion-stack colour map; the first cycle found
35
+ * wins (enough to report — the author fixes one at a time).
36
+ */
37
+ export function findCycle(nodeIds, edges) {
38
+ const adj = new Map();
39
+ for (const id of nodeIds)
40
+ adj.set(id, []);
41
+ for (const e of edges) {
42
+ if (adj.has(e.source) && adj.has(e.target))
43
+ adj.get(e.source).push(e.target);
44
+ }
45
+ const WHITE = 0, GRAY = 1, BLACK = 2;
46
+ const color = new Map(nodeIds.map((id) => [id, WHITE]));
47
+ const stack = [];
48
+ const visit = (start) => {
49
+ // Explicit stack of {node, next-child-index} frames so a deep graph can't
50
+ // blow the JS call stack.
51
+ const frames = [{ id: start, i: 0 }];
52
+ color.set(start, GRAY);
53
+ stack.push(start);
54
+ while (frames.length) {
55
+ const frame = frames[frames.length - 1];
56
+ const children = adj.get(frame.id) ?? [];
57
+ if (frame.i < children.length) {
58
+ const next = children[frame.i++];
59
+ const c = color.get(next);
60
+ if (c === GRAY) {
61
+ // Back into the active path → cycle. Slice from `next` to close it.
62
+ const from = stack.indexOf(next);
63
+ return [...stack.slice(from), next];
64
+ }
65
+ if (c === WHITE) {
66
+ color.set(next, GRAY);
67
+ stack.push(next);
68
+ frames.push({ id: next, i: 0 });
69
+ }
70
+ }
71
+ else {
72
+ color.set(frame.id, BLACK);
73
+ stack.pop();
74
+ frames.pop();
75
+ }
76
+ }
77
+ return null;
78
+ };
79
+ for (const id of nodeIds) {
80
+ if (color.get(id) === WHITE) {
81
+ const cycle = visit(id);
82
+ if (cycle)
83
+ return cycle;
84
+ }
85
+ }
86
+ return null;
87
+ }
31
88
  /** Static structural checks; `errors` block Run, `warnings` are advisory. */
32
89
  export function validateFlowDraft(nodes, edges) {
33
90
  const errors = [];
@@ -109,5 +166,20 @@ export function validateFlowDraft(nodes, edges) {
109
166
  }
110
167
  }
111
168
  }
169
+ // DAG-modulo-back-edges (ADR-0044): the engine requires the flow graph MINUS
170
+ // declared back-edges to be acyclic. A declared revise loop (its closing edge
171
+ // marked `type: 'back'`) is excluded and passes; any *unmarked* cycle is an
172
+ // error — the author must opt in, edge by edge, exactly as `registerFlow`
173
+ // enforces server-side.
174
+ const forwardEdges = edges.filter((e) => e.type !== 'back');
175
+ const cycle = findCycle(ids.filter((id) => !!id), forwardEdges);
176
+ if (cycle) {
177
+ errors.push({
178
+ level: 'error',
179
+ nodeId: cycle[0],
180
+ cycle,
181
+ message: `Cycle detected (${cycle.join(' → ')}). Mark the connection that closes the loop as a back-edge (Connection type → Back-edge) to declare an intentional revise/rework loop.`,
182
+ });
183
+ }
112
184
  return { errors, warnings, startNodeId };
113
185
  }
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * Supported faithfully: start, decision (edge-first CEL routing), the CRUD /
6
6
  * get / http / connector / script(notification|code) side-effects (MOCKED), and
7
- * end. `wait` and `screen` PAUSE for manual continuation. `loop` resolves its
7
+ * end. `wait`, `screen`, and `approval` PAUSE for manual continuation (an approval
8
+ * resumes down the chosen decision's branch — approve / reject / revise). `loop` resolves its
8
9
  * collection and exposes the iterator but is a labelled single pass (the edge
9
10
  * model has no separate body/exit edge). `parallel_gateway` fans out WITHOUT
10
11
  * join synchronization; the ADR-0031 structured containers (`parallel`,
@@ -27,10 +28,38 @@ export declare class FlowSimulator {
27
28
  step(): SimStep | null;
28
29
  /** Run to completion (or until a pause / error). */
29
30
  runToEnd(): SimState;
30
- /** Continue a flow paused on a `wait` / `screen` node. */
31
- resume(screenOutputs?: Record<string, unknown>): void;
31
+ /**
32
+ * Continue a flow paused on a `wait`, `screen`, or `approval` node.
33
+ * - `screenOutputs` — inputs captured from a paused screen.
34
+ * - `decision` — the branch an approval resumes down (ADR-0019/0044:
35
+ * `approve` / `reject` / `revise`). The run takes ONLY the out-edge whose
36
+ * label matches, mirroring how the engine resumes a suspended approval by
37
+ * branch label — instead of fanning out to every out-edge.
38
+ */
39
+ resume(opts?: {
40
+ screenOutputs?: Record<string, unknown>;
41
+ decision?: string;
42
+ }): void;
43
+ /**
44
+ * Resume a suspended approval down the chosen decision's out-edge: the one
45
+ * whose `label` equals `decision` (case-insensitive — `approve` / `reject` /
46
+ * `revise`). With no match (or no decision) it falls back to fanning out —
47
+ * mirroring the engine's unmatched-`branchLabel` fallback — and logs that so
48
+ * the author notices the unrouted decision.
49
+ */
50
+ private resumeApproval;
32
51
  private execute;
33
52
  private executeDecision;
53
+ /**
54
+ * assignment node — set flow variables. Normalizes the three authoring
55
+ * shapes the engine accepts (Studio's `{ assignments: { var: value } }`
56
+ * map, the example `{ assignments: [{ variable, value }] }` array, and the
57
+ * legacy flat `{ var: value }`) and interpolates `{var}` templates — so the
58
+ * Debug run mirrors runtime instead of silently no-oping.
59
+ */
60
+ private executeAssignment;
61
+ /** Resolve `{var}` templates in an assignment value against live variables. */
62
+ private interpolateValue;
34
63
  private executeLoop;
35
64
  /** Resolve a `{var}` template ref or a plain variable name from `variables`. */
36
65
  private resolveRef;
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
  import { evalCondition, validateFlowDraft } from './flow-sim-validate';
3
3
  const MAX_STEPS = 500;
4
- const PASS_THROUGH = new Set(['start', 'assignment']);
4
+ const PASS_THROUGH = new Set(['start']);
5
5
  const MOCKED_SIDE_EFFECT = new Set([
6
6
  'create_record',
7
7
  'update_record',
@@ -124,24 +124,58 @@ export class FlowSimulator {
124
124
  }
125
125
  return this.state;
126
126
  }
127
- /** Continue a flow paused on a `wait` / `screen` node. */
128
- resume(screenOutputs) {
127
+ /**
128
+ * Continue a flow paused on a `wait`, `screen`, or `approval` node.
129
+ * - `screenOutputs` — inputs captured from a paused screen.
130
+ * - `decision` — the branch an approval resumes down (ADR-0019/0044:
131
+ * `approve` / `reject` / `revise`). The run takes ONLY the out-edge whose
132
+ * label matches, mirroring how the engine resumes a suspended approval by
133
+ * branch label — instead of fanning out to every out-edge.
134
+ */
135
+ resume(opts = {}) {
129
136
  const s = this.state;
130
137
  if (s.status !== 'paused' || !s.activeNodeId)
131
138
  return;
132
139
  const node = this.nodes.get(s.activeNodeId);
133
- if (screenOutputs && node?.type === 'screen') {
134
- Object.assign(s.variables, screenOutputs);
140
+ if (opts.screenOutputs && node?.type === 'screen') {
141
+ Object.assign(s.variables, opts.screenOutputs);
135
142
  }
136
143
  s.pausedReason = undefined;
137
144
  s.status = 'running';
138
- if (node)
145
+ if (node?.type === 'approval')
146
+ this.resumeApproval(node, opts.decision);
147
+ else if (node)
139
148
  this.enqueueSuccessors(node);
140
149
  if (s.frontier.length === 0) {
141
150
  s.status = 'done';
142
151
  s.activeNodeId = null;
143
152
  }
144
153
  }
154
+ /**
155
+ * Resume a suspended approval down the chosen decision's out-edge: the one
156
+ * whose `label` equals `decision` (case-insensitive — `approve` / `reject` /
157
+ * `revise`). With no match (or no decision) it falls back to fanning out —
158
+ * mirroring the engine's unmatched-`branchLabel` fallback — and logs that so
159
+ * the author notices the unrouted decision.
160
+ */
161
+ resumeApproval(node, decision) {
162
+ const out = this.edges.map((e, i) => ({ e, i })).filter((x) => x.e.source === node.id);
163
+ const want = (decision ?? '').trim().toLowerCase();
164
+ const chosen = want ? out.find((x) => (x.e.label ?? '').trim().toLowerCase() === want) : undefined;
165
+ if (chosen) {
166
+ this.traverse(chosen.e, chosen.i);
167
+ this.record(node.id, 'approval', node.label, 'ok', { note: `Decision: ${decision} → ${chosen.e.target}` });
168
+ }
169
+ else {
170
+ for (const x of out)
171
+ this.traverse(x.e, x.i);
172
+ this.record(node.id, 'approval', node.label, 'ok', {
173
+ note: want
174
+ ? `No out-edge labelled "${decision}"; took all branches (engine label-fallback).`
175
+ : 'No decision supplied; took all branches.',
176
+ });
177
+ }
178
+ }
145
179
  // ---- node execution -----------------------------------------------------
146
180
  execute(node) {
147
181
  const type = node.type;
@@ -151,6 +185,8 @@ export class FlowSimulator {
151
185
  }
152
186
  if (type === 'decision')
153
187
  return this.executeDecision(node);
188
+ if (type === 'assignment')
189
+ return this.executeAssignment(node);
154
190
  if (UNSUPPORTED.has(type)) {
155
191
  this.enqueueSuccessors(node);
156
192
  return this.record(node.id, type, node.label, 'skipped', {
@@ -163,15 +199,34 @@ export class FlowSimulator {
163
199
  note: 'Parallel split — branches fan out (no join synchronization is simulated).',
164
200
  });
165
201
  }
202
+ if (type === 'approval') {
203
+ // ADR-0019: an approval node opens a request and SUSPENDS the run until a
204
+ // decision is recorded. Model that as a pause; the author resumes down the
205
+ // chosen approve / reject / revise out-edge (see resumeApproval) rather
206
+ // than fanning out to every out-edge at once.
207
+ this.state.status = 'paused';
208
+ this.state.pausedReason = 'approval';
209
+ return this.record(node.id, type, node.label, 'paused', { note: 'Approval reached — choose a decision to continue.' });
210
+ }
166
211
  if (type === 'wait') {
167
212
  this.state.status = 'paused';
168
213
  this.state.pausedReason = 'wait';
169
214
  return this.record(node.id, type, node.label, 'paused', { note: 'Wait reached — continue manually.' });
170
215
  }
171
216
  if (type === 'screen') {
172
- this.state.status = 'paused';
173
- this.state.pausedReason = 'screen';
174
- return this.record(node.id, type, node.label, 'paused', { note: 'Screen reached provide inputs, then continue.' });
217
+ // Mirror the engine's `shouldPause`: a screen suspends only when it
218
+ // collects input (`fields`) or explicitly opts in (`waitForInput`).
219
+ // A field-less / `waitForInput:false` screen is a server pass-through.
220
+ const fields = Array.isArray(node.config?.fields) ? node.config.fields : [];
221
+ const waitForInput = node.config?.waitForInput;
222
+ const shouldPause = waitForInput === true || (fields.length > 0 && waitForInput !== false);
223
+ if (shouldPause) {
224
+ this.state.status = 'paused';
225
+ this.state.pausedReason = 'screen';
226
+ return this.record(node.id, type, node.label, 'paused', { note: 'Screen reached — provide inputs, then continue.' });
227
+ }
228
+ this.enqueueSuccessors(node);
229
+ return this.record(node.id, type, node.label, 'ok', { note: 'Screen has no input — passed through (matches runtime).' });
175
230
  }
176
231
  if (type === 'loop') {
177
232
  const step = this.executeLoop(node);
@@ -233,6 +288,61 @@ export class FlowSimulator {
233
288
  note: multiMatch ? 'Multiple conditions matched; the first declared branch was taken.' : undefined,
234
289
  });
235
290
  }
291
+ /**
292
+ * assignment node — set flow variables. Normalizes the three authoring
293
+ * shapes the engine accepts (Studio's `{ assignments: { var: value } }`
294
+ * map, the example `{ assignments: [{ variable, value }] }` array, and the
295
+ * legacy flat `{ var: value }`) and interpolates `{var}` templates — so the
296
+ * Debug run mirrors runtime instead of silently no-oping.
297
+ */
298
+ executeAssignment(node) {
299
+ const cfg = node.config ?? {};
300
+ const raw = cfg.assignments;
301
+ const pairs = [];
302
+ if (Array.isArray(raw)) {
303
+ for (const item of raw) {
304
+ if (item && typeof item === 'object') {
305
+ const e = item;
306
+ const name = e.variable ?? e.name ?? e.key;
307
+ if (typeof name === 'string' && name)
308
+ pairs.push([name, e.value]);
309
+ }
310
+ }
311
+ }
312
+ else if (raw && typeof raw === 'object') {
313
+ for (const [k, v] of Object.entries(raw))
314
+ pairs.push([k, v]);
315
+ }
316
+ else {
317
+ for (const [k, v] of Object.entries(cfg))
318
+ pairs.push([k, v]);
319
+ }
320
+ const wrote = {};
321
+ for (const [key, value] of pairs) {
322
+ const resolved = this.interpolateValue(value);
323
+ this.state.variables[key] = resolved;
324
+ wrote[key] = resolved;
325
+ }
326
+ this.enqueueSuccessors(node);
327
+ return this.record(node.id, 'assignment', node.label, 'ok', {
328
+ wrote: Object.keys(wrote).length ? wrote : undefined,
329
+ note: Object.keys(wrote).length ? undefined : 'No assignments defined.',
330
+ });
331
+ }
332
+ /** Resolve `{var}` templates in an assignment value against live variables. */
333
+ interpolateValue(value) {
334
+ if (typeof value !== 'string')
335
+ return value;
336
+ const whole = value.match(/^\{([^}]+)\}$/);
337
+ if (whole) {
338
+ const v = this.state.variables[whole[1].trim()];
339
+ return v !== undefined ? v : value;
340
+ }
341
+ return value.replace(/\{([^}]+)\}/g, (_m, k) => {
342
+ const v = this.state.variables[String(k).trim()];
343
+ return v === undefined || v === null ? '' : String(v);
344
+ });
345
+ }
236
346
  executeLoop(node) {
237
347
  const ref = str(node.config?.collection);
238
348
  const iterVar = str(node.config?.iteratorVariable);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
@@ -33,36 +33,36 @@
33
33
  "qrcode": "^1.5.4",
34
34
  "sonner": "^2.0.7",
35
35
  "zod": "^4.4.3",
36
- "@object-ui/auth": "7.0.0",
37
- "@object-ui/collaboration": "7.0.0",
38
- "@object-ui/components": "7.0.0",
39
- "@object-ui/core": "7.0.0",
40
- "@object-ui/data-objectstack": "7.0.0",
41
- "@object-ui/fields": "7.0.0",
42
- "@object-ui/i18n": "7.0.0",
43
- "@object-ui/layout": "7.0.0",
44
- "@object-ui/permissions": "7.0.0",
45
- "@object-ui/plugin-editor": "7.0.0",
46
- "@object-ui/providers": "7.0.0",
47
- "@object-ui/react": "7.0.0",
48
- "@object-ui/types": "7.0.0"
36
+ "@object-ui/auth": "7.1.0",
37
+ "@object-ui/collaboration": "7.1.0",
38
+ "@object-ui/components": "7.1.0",
39
+ "@object-ui/core": "7.1.0",
40
+ "@object-ui/data-objectstack": "7.1.0",
41
+ "@object-ui/fields": "7.1.0",
42
+ "@object-ui/i18n": "7.1.0",
43
+ "@object-ui/layout": "7.1.0",
44
+ "@object-ui/permissions": "7.1.0",
45
+ "@object-ui/plugin-editor": "7.1.0",
46
+ "@object-ui/providers": "7.1.0",
47
+ "@object-ui/react": "7.1.0",
48
+ "@object-ui/types": "7.1.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": "^18.0.0 || ^19.0.0",
52
52
  "react-dom": "^18.0.0 || ^19.0.0",
53
53
  "react-router-dom": "^6.0.0 || ^7.0.0",
54
- "@object-ui/plugin-calendar": "^7.0.0",
55
- "@object-ui/plugin-charts": "^7.0.0",
56
- "@object-ui/plugin-chatbot": "^7.0.0",
57
- "@object-ui/plugin-dashboard": "^7.0.0",
58
- "@object-ui/plugin-designer": "^7.0.0",
59
- "@object-ui/plugin-detail": "^7.0.0",
60
- "@object-ui/plugin-form": "^7.0.0",
61
- "@object-ui/plugin-grid": "^7.0.0",
62
- "@object-ui/plugin-kanban": "^7.0.0",
63
- "@object-ui/plugin-list": "^7.0.0",
64
- "@object-ui/plugin-report": "^7.0.0",
65
- "@object-ui/plugin-view": "^7.0.0"
54
+ "@object-ui/plugin-calendar": "^7.1.0",
55
+ "@object-ui/plugin-charts": "^7.1.0",
56
+ "@object-ui/plugin-chatbot": "^7.1.0",
57
+ "@object-ui/plugin-dashboard": "^7.1.0",
58
+ "@object-ui/plugin-designer": "^7.1.0",
59
+ "@object-ui/plugin-detail": "^7.1.0",
60
+ "@object-ui/plugin-form": "^7.1.0",
61
+ "@object-ui/plugin-grid": "^7.1.0",
62
+ "@object-ui/plugin-kanban": "^7.1.0",
63
+ "@object-ui/plugin-list": "^7.1.0",
64
+ "@object-ui/plugin-report": "^7.1.0",
65
+ "@object-ui/plugin-view": "^7.1.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^26.0.0",
@@ -75,18 +75,18 @@
75
75
  "sonner": "^2.0.7",
76
76
  "typescript": "^6.0.3",
77
77
  "vite": "^8.0.16",
78
- "@object-ui/plugin-calendar": "7.0.0",
79
- "@object-ui/plugin-charts": "7.0.0",
80
- "@object-ui/plugin-chatbot": "7.0.0",
81
- "@object-ui/plugin-dashboard": "7.0.0",
82
- "@object-ui/plugin-designer": "7.0.0",
83
- "@object-ui/plugin-detail": "7.0.0",
84
- "@object-ui/plugin-form": "7.0.0",
85
- "@object-ui/plugin-grid": "7.0.0",
86
- "@object-ui/plugin-kanban": "7.0.0",
87
- "@object-ui/plugin-list": "7.0.0",
88
- "@object-ui/plugin-report": "7.0.0",
89
- "@object-ui/plugin-view": "7.0.0"
78
+ "@object-ui/plugin-calendar": "7.1.0",
79
+ "@object-ui/plugin-charts": "7.1.0",
80
+ "@object-ui/plugin-chatbot": "7.1.0",
81
+ "@object-ui/plugin-dashboard": "7.1.0",
82
+ "@object-ui/plugin-designer": "7.1.0",
83
+ "@object-ui/plugin-detail": "7.1.0",
84
+ "@object-ui/plugin-form": "7.1.0",
85
+ "@object-ui/plugin-grid": "7.1.0",
86
+ "@object-ui/plugin-kanban": "7.1.0",
87
+ "@object-ui/plugin-list": "7.1.0",
88
+ "@object-ui/plugin-report": "7.1.0",
89
+ "@object-ui/plugin-view": "7.1.0"
90
90
  },
91
91
  "keywords": [
92
92
  "objectui",