@xdelivered/emberflow 0.2.0 → 0.5.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 (116) hide show
  1. package/bin/commands.ts +7 -3
  2. package/bin/init.test.ts +94 -0
  3. package/bin/init.ts +108 -2
  4. package/dist/bin/commands.d.ts +1 -0
  5. package/dist/bin/commands.js +6 -3
  6. package/dist/bin/init.d.ts +4 -0
  7. package/dist/bin/init.js +96 -2
  8. package/dist/server/agents/codexParse.js +31 -0
  9. package/dist/server/agents/detect.d.ts +18 -8
  10. package/dist/server/agents/detect.js +78 -13
  11. package/dist/server/agents/modelRejectionHint.js +1 -1
  12. package/dist/server/agents/prompt.d.ts +15 -1
  13. package/dist/server/agents/prompt.js +59 -37
  14. package/dist/server/agents/runManager.d.ts +23 -2
  15. package/dist/server/agents/runManager.js +36 -8
  16. package/dist/server/client.d.ts +1 -0
  17. package/dist/server/client.js +7 -2
  18. package/dist/server/index.js +208 -16
  19. package/dist/server/nodesPayload.d.ts +14 -1
  20. package/dist/server/nodesPayload.js +15 -2
  21. package/dist/server/projectMode.js +5 -2
  22. package/dist/server/runRegistry.d.ts +52 -1
  23. package/dist/server/runRegistry.js +170 -1
  24. package/dist/server/sourceNav.d.ts +77 -0
  25. package/dist/server/sourceNav.js +503 -0
  26. package/dist/server/subflowRunner.d.ts +48 -1
  27. package/dist/server/subflowRunner.js +34 -7
  28. package/dist/server/updateCheck.d.ts +68 -0
  29. package/dist/server/updateCheck.js +142 -0
  30. package/dist/src/engine/executor.js +4 -3
  31. package/dist/src/engine/registry.d.ts +27 -2
  32. package/dist/src/engine/registry.js +84 -2
  33. package/dist/src/nodes/index.d.ts +8 -2
  34. package/dist/src/nodes/index.js +7 -3
  35. package/dist/src/nodes/login.d.ts +3 -1
  36. package/dist/src/nodes/login.js +2 -2
  37. package/package.json +28 -7
  38. package/server/agentRoute.test.ts +20 -0
  39. package/server/agents/codexParse.test.ts +37 -0
  40. package/server/agents/codexParse.ts +34 -0
  41. package/server/agents/detect.test.ts +26 -7
  42. package/server/agents/detect.ts +82 -14
  43. package/server/agents/modelRejectionHint.ts +2 -1
  44. package/server/agents/prompt.test.ts +128 -0
  45. package/server/agents/prompt.ts +102 -3
  46. package/server/agents/runManager.test.ts +9 -0
  47. package/server/agents/runManager.ts +37 -7
  48. package/server/client.ts +10 -4
  49. package/server/index.ts +213 -15
  50. package/server/nodeRun.test.ts +189 -0
  51. package/server/nodesPayload.test.ts +38 -0
  52. package/server/nodesPayload.ts +28 -3
  53. package/server/projectMode.ts +5 -2
  54. package/server/runRegistry.ts +200 -3
  55. package/server/setupStatus.test.ts +3 -0
  56. package/server/sourceNav.test.ts +382 -0
  57. package/server/sourceNav.ts +644 -0
  58. package/server/sourceNavRoute.test.ts +163 -0
  59. package/server/stepDrill.test.ts +380 -0
  60. package/server/subflowRunner.ts +92 -11
  61. package/server/updateCheck.test.ts +209 -0
  62. package/server/updateCheck.ts +175 -0
  63. package/server/workflowTestRoute.test.ts +1 -1
  64. package/src/App.tsx +82 -2
  65. package/src/components/AgentConsole.tsx +7 -280
  66. package/src/components/AgentStream.test.tsx +88 -0
  67. package/src/components/AgentStream.tsx +303 -0
  68. package/src/components/CreateModal.tsx +43 -9
  69. package/src/components/Dock.tsx +1 -1
  70. package/src/components/EmptyState.test.tsx +49 -0
  71. package/src/components/EmptyState.tsx +61 -0
  72. package/src/components/EnvironmentDialog.tsx +4 -1
  73. package/src/components/EnvironmentPicker.tsx +8 -3
  74. package/src/components/EnvironmentsDialog.tsx +4 -1
  75. package/src/components/InfraPanel.tsx +30 -2
  76. package/src/components/InfrastructureDialog.test.tsx +97 -0
  77. package/src/components/InfrastructureDialog.tsx +111 -0
  78. package/src/components/Inspector.tsx +9 -26
  79. package/src/components/RunbookView.tsx +70 -6
  80. package/src/components/SettingsDialog.tsx +4 -59
  81. package/src/components/Sidebar.tsx +6 -26
  82. package/src/components/SourceNavigator.test.tsx +226 -0
  83. package/src/components/SourceNavigator.tsx +520 -0
  84. package/src/components/StatusBar.tsx +227 -26
  85. package/src/components/Toolbar.tsx +28 -16
  86. package/src/components/UpdateChip.test.tsx +31 -0
  87. package/src/components/WelcomeDialog.test.tsx +384 -13
  88. package/src/components/WelcomeDialog.tsx +996 -177
  89. package/src/engine/executor.ts +4 -3
  90. package/src/engine/registry.sourceRef.test.ts +112 -0
  91. package/src/engine/registry.ts +103 -2
  92. package/src/lib/guidedQuestions.test.ts +224 -0
  93. package/src/lib/guidedQuestions.ts +181 -0
  94. package/src/lib/runbookModel.ts +3 -3
  95. package/src/lib/runbookProjection.test.ts +43 -0
  96. package/src/lib/runbookProjection.ts +26 -6
  97. package/src/nodes/index.ts +10 -3
  98. package/src/nodes/login.ts +5 -2
  99. package/src/store/agentClient.ts +27 -8
  100. package/src/store/apiTree.ts +12 -0
  101. package/src/store/builderStore.test.ts +441 -99
  102. package/src/store/builderStore.ts +385 -314
  103. package/src/store/nodeMeta.ts +10 -2
  104. package/src/store/serverRunner.ts +56 -5
  105. package/src/store/setupClient.ts +7 -2
  106. package/src/store/sourceNavClient.ts +48 -0
  107. package/src/store/updateClient.ts +50 -0
  108. package/studio-dist/assets/index-BbzppDpt.js +65 -0
  109. package/studio-dist/assets/index-CLf6blcA.css +1 -0
  110. package/studio-dist/index.html +2 -2
  111. package/templates/skills/emberflow-basics/SKILL.md +29 -1
  112. package/templates/skills/emberflow-model-process/SKILL.md +92 -9
  113. package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
  114. package/templates/skills/emberflow-review-workflow/SKILL.md +64 -1
  115. package/studio-dist/assets/index-DNJwW-hM.css +0 -1
  116. package/studio-dist/assets/index-O26dKRjW.js +0 -65
@@ -1,6 +1,6 @@
1
- import type { WorkflowDefinition, WorkflowEdge } from '../engine';
2
- import { computeLoopRegions, topoSort, type LoopRegion } from '../engine';
3
- import type { NodeRegistry } from '../engine';
1
+ import type { WorkflowDefinition, WorkflowEdge } from '../engine/types';
2
+ import { computeLoopRegions, topoSort, type LoopRegion } from '../engine/validation';
3
+ import type { NodeRegistry } from '../engine/registry';
4
4
  import { firstSentence, simpleNodeDescription } from './registerLens';
5
5
 
6
6
  export interface RunbookStep {
@@ -209,6 +209,49 @@ describe('projectRunbook', () => {
209
209
  expect(proj.steps.get('onStep')!.tech).toBe(`#2 Http · 50ms · ERROR: ${'x'.repeat(80)}`);
210
210
  });
211
211
 
212
+ it('drilled view: outcome and receipt match caller-prefixed child log ids (`caller/nodeId`); undrilled does not', () => {
213
+ const doc = makeDoc();
214
+ const run = makeRun({
215
+ nodeStates: {
216
+ onStep: {
217
+ status: 'succeeded',
218
+ startedAt: '2026-01-01T00:00:00.000Z',
219
+ completedAt: '2026-01-01T00:00:00.100Z',
220
+ input: { a: 1 },
221
+ output: { x: 1 },
222
+ },
223
+ },
224
+ });
225
+ // How a drilled subflow child's logs arrive: nodeId prefixed with the
226
+ // caller chain (`sub/onStep`, nested per level).
227
+ const logs: LogLine[] = [
228
+ { timestamp: 't1', level: 'debug', runId: 'run-1', nodeId: 'sub/onStep', message: '#4 ▶ execute' },
229
+ { timestamp: 't2', level: 'info', runId: 'run-1', nodeId: 'sub/onStep', message: 'child outcome' },
230
+ ];
231
+
232
+ const drilled = projectRunbook(doc, run, logs, [], FLOW_ID, true);
233
+ expect(drilled.steps.get('onStep')!.outcome).toBe('child outcome');
234
+ expect(drilled.steps.get('onStep')!.tech).toBe('#4 Http · 100ms · in[a] → out[x]');
235
+
236
+ // Undrilled: suffix matching stays off so a parent node can never pick
237
+ // up a same-named child's prefixed lines.
238
+ const plain = projectRunbook(doc, run, logs, [], FLOW_ID);
239
+ expect(plain.steps.get('onStep')!.outcome).toBe('');
240
+ expect(plain.steps.get('onStep')!.tech).toBe('Http · 100ms · in[a] → out[x]');
241
+ });
242
+
243
+ it('drilled view: a plain (unprefixed) id must not suffix-match another node ending with the same segment', () => {
244
+ const doc = makeDoc();
245
+ const logs: LogLine[] = [
246
+ // `sub/onStep` ends with `/onStep` but NOT with `/Step` etc.; also an
247
+ // exact-id line for a different node must not bleed over.
248
+ { timestamp: 't1', level: 'info', runId: 'run-1', nodeId: 'cond', message: 'cond outcome' },
249
+ ];
250
+ const drilled = projectRunbook(doc, makeRun(), logs, [], FLOW_ID, true);
251
+ expect(drilled.steps.get('onStep')!.outcome).toBe('');
252
+ expect(drilled.steps.get('cond')!.outcome).toBe('cond outcome');
253
+ });
254
+
212
255
  it('tech line: no receipt yet means no #N prefix', () => {
213
256
  const doc = makeDoc();
214
257
  const run = makeRun({ nodeStates: { onStep: { status: 'idle' } } });
@@ -56,10 +56,24 @@ function fieldList(value: unknown, label: string): string {
56
56
  return `${label}[${shown.join(',')}]`;
57
57
  }
58
58
 
59
- function lastInfoOutcome(logs: LogLine[], nodeId: string): string {
59
+ /**
60
+ * Whether a log line belongs to the given node. A drilled-in subflow child's
61
+ * log lines carry nodeIds prefixed with the caller chain
62
+ * (`${callerNodeId}/${childNodeId}`, nested per level — see subflowRunner),
63
+ * while the drilled view's runbook uses the raw child ids, so when projecting
64
+ * a drilled child flow a suffix match after a slash also counts. Kept behind
65
+ * `drilled` so an undriled parent view can never match a child's prefixed
66
+ * line against a same-named parent node.
67
+ */
68
+ function lineIsFor(line: LogLine, nodeId: string, drilled: boolean): boolean {
69
+ if (line.nodeId === nodeId) return true;
70
+ return drilled && !!line.nodeId && line.nodeId.endsWith(`/${nodeId}`);
71
+ }
72
+
73
+ function lastInfoOutcome(logs: LogLine[], nodeId: string, drilled: boolean): string {
60
74
  for (let i = logs.length - 1; i >= 0; i--) {
61
75
  const line = logs[i];
62
- if (line.nodeId === nodeId && line.level === 'info') return line.message;
76
+ if (lineIsFor(line, nodeId, drilled) && line.level === 'info') return line.message;
63
77
  }
64
78
  return '';
65
79
  }
@@ -72,10 +86,10 @@ function lastInfoOutcome(logs: LogLine[], nodeId: string): string {
72
86
  * iteration); null when the node has no receipt yet (never executed).
73
87
  */
74
88
  const RECEIPT_RE = /^#(\d+) ▶ execute/;
75
- function receiptSeq(logs: LogLine[], nodeId: string): string | null {
89
+ function receiptSeq(logs: LogLine[], nodeId: string, drilled: boolean): string | null {
76
90
  for (let i = logs.length - 1; i >= 0; i--) {
77
91
  const line = logs[i];
78
- if (line.nodeId === nodeId && line.level === 'debug') {
92
+ if (lineIsFor(line, nodeId, drilled) && line.level === 'debug') {
79
93
  const match = RECEIPT_RE.exec(line.message);
80
94
  if (match) return match[1];
81
95
  }
@@ -152,6 +166,9 @@ export function projectRunbook(
152
166
  logs: LogLine[],
153
167
  history: RunHistoryEntry[],
154
168
  flowId: string,
169
+ /** True when projecting a drilled-in subflow child view: log lines then
170
+ * also match by `…/${nodeId}` suffix (child log ids are caller-prefixed). */
171
+ drilled = false,
155
172
  ): RunbookProjection {
156
173
  // ── steps ──
157
174
  const steps = new Map<string, StepProjection>();
@@ -160,10 +177,13 @@ export function projectRunbook(
160
177
  const status = statusOf(state);
161
178
  // A failed node's outcome line is its error, not the last info log (which
162
179
  // may be stale from a prior successful attempt or simply absent).
163
- const outcome = status === 'failed' ? state?.error || lastInfoOutcome(logs, nodeId) : lastInfoOutcome(logs, nodeId);
180
+ const outcome =
181
+ status === 'failed'
182
+ ? state?.error || lastInfoOutcome(logs, nodeId, drilled)
183
+ : lastInfoOutcome(logs, nodeId, drilled);
164
184
  const durationMs = durationOf(state);
165
185
  // `#N` receipt prefix once the node has executed; bare type name before that.
166
- const seq = receiptSeq(logs, nodeId);
186
+ const seq = receiptSeq(logs, nodeId, drilled);
167
187
  const prefix = seq ? `#${seq} ` : '';
168
188
  const ms = `${durationMs ?? 0}ms`;
169
189
  let tech: string;
@@ -8,9 +8,16 @@ import { registerPradarNodesModule } from './pradar';
8
8
  import { registerResponseNodes } from './response';
9
9
  import { registerRequireAuthNode } from './requireAuth';
10
10
 
11
- /** All built-in nodes: login examples, Open-Meteo weather, anomaly-detection API, EV charging demo, HTTP Response, requireAuth. */
12
- export function createDefaultRegistry(delayMs?: number): NodeRegistry {
13
- const registry = createLoginRegistry(delayMs);
11
+ /**
12
+ * All built-in nodes: login examples, Open-Meteo weather, anomaly-detection API, EV charging demo, HTTP Response, requireAuth.
13
+ * `opts.captureSourceRefs` is threaded from SERVER callers only (buildRegistries)
14
+ * — this function is shared with the browser bundle, where capture stays off.
15
+ */
16
+ export function createDefaultRegistry(
17
+ delayMs?: number,
18
+ opts?: { captureSourceRefs?: boolean },
19
+ ): NodeRegistry {
20
+ const registry = createLoginRegistry(delayMs, opts);
14
21
  registerWeatherNodes(registry);
15
22
  registerAnomalyNodes(registry);
16
23
  registerFlowControlNodes(registry);
@@ -12,8 +12,11 @@ function usernamePart(userId: string): string {
12
12
  * `delayMs` is injected into every implementation via `await sleep(delayMs)`
13
13
  * so tests can run with no delay by passing `0`.
14
14
  */
15
- export function createLoginRegistry(delayMs = 300): NodeRegistry {
16
- const registry = new NodeRegistry();
15
+ export function createLoginRegistry(
16
+ delayMs = 300,
17
+ opts?: { captureSourceRefs?: boolean },
18
+ ): NodeRegistry {
19
+ const registry = new NodeRegistry(opts);
17
20
 
18
21
  registry.register(
19
22
  {
@@ -40,6 +40,7 @@ export type AgentIntent =
40
40
  | { action: 'setup-auth'; environment: string; instruction: string }
41
41
  | { action: 'setup-environments'; instruction: string }
42
42
  | { action: 'scout-infrastructure'; instruction: string }
43
+ | { action: 'guided-setup'; instruction: string }
43
44
  | { action: 'cover-operation'; flowId: string; instruction: string }
44
45
  | { action: 'ask'; flowId?: string; instruction: string };
45
46
 
@@ -63,11 +64,18 @@ export async function fetchAvailableAgents(): Promise<DetectedAgent[]> {
63
64
 
64
65
  /** POST /agent — start an agent run, returns its id. */
65
66
  export async function startAgent(intent: AgentIntent, opts?: StartAgentOptions): Promise<string> {
66
- const response = await fetch(`${BASE}/agent`, {
67
- method: 'POST',
68
- headers: { 'content-type': 'application/json' },
69
- body: JSON.stringify({ intent, ...opts }),
70
- });
67
+ let response: Response;
68
+ try {
69
+ response = await fetch(`${BASE}/agent`, {
70
+ method: 'POST',
71
+ headers: { 'content-type': 'application/json' },
72
+ body: JSON.stringify({ intent, ...opts }),
73
+ });
74
+ } catch {
75
+ // fetch rejects with a bare "Failed to fetch" on network errors — say what
76
+ // that actually means here and what to do about it.
77
+ throw new Error('Could not reach the runner — check it is still running, then try again.');
78
+ }
71
79
  if (!response.ok) {
72
80
  const body = (await response.json().catch(() => ({}))) as { error?: string };
73
81
  throw new Error(body.error ?? `Failed to start agent (HTTP ${response.status})`);
@@ -84,16 +92,27 @@ export async function startAgent(intent: AgentIntent, opts?: StartAgentOptions):
84
92
  */
85
93
  export function streamAgent(agentRunId: string, onEvent: (event: AgentEvent) => void): () => void {
86
94
  const source = new EventSource(`${BASE}/agent/${agentRunId}/events`);
95
+ // Set once the server delivers the run's terminal event — from then on any
96
+ // connection-level error (the server closing the response right after) is
97
+ // expected, not a lost stream, so onerror must stay quiet.
98
+ let sawTerminal = false;
87
99
  const types: AgentEvent['type'][] = ['started', 'message', 'command', 'mcp', 'approval-request', 'done', 'error'];
88
100
  for (const type of types) {
89
101
  source.addEventListener(type, (event) => {
90
- const data = JSON.parse((event as MessageEvent).data) as AgentEvent;
102
+ // EventSource dispatches its CONNECTION errors as `error` events too —
103
+ // those carry no data, unlike the server's named `event: error` payloads.
104
+ const raw = (event as MessageEvent).data as string | undefined;
105
+ if (raw === undefined) return;
106
+ const data = JSON.parse(raw) as AgentEvent;
91
107
  onEvent(data);
92
- if (type === 'done' || type === 'error') source.close();
108
+ if (type === 'done' || type === 'error') {
109
+ sawTerminal = true;
110
+ source.close();
111
+ }
93
112
  });
94
113
  }
95
114
  source.onerror = () => {
96
- if (source.readyState === EventSource.CLOSED) {
115
+ if (!sawTerminal && source.readyState === EventSource.CLOSED) {
97
116
  onEvent({ type: 'error', text: 'Lost connection to the agent event stream' });
98
117
  }
99
118
  };
@@ -71,3 +71,15 @@ export function buildApiTree(ops: OpInput[]): ApiTreeNode[] {
71
71
  tree.sort((a, b) => a.name.localeCompare(b.name));
72
72
  return tree;
73
73
  }
74
+
75
+ /** Flatten an API tree into every API/folder path (e.g. 'default', 'billing', 'billing/charges'). */
76
+ export function flattenLocations(tree: ApiTreeNode[]): string[] {
77
+ const paths: string[] = [];
78
+ const walk = (node: ApiTreeNode, prefix: string) => {
79
+ const path = prefix ? `${prefix}/${node.name}` : node.name;
80
+ paths.push(path);
81
+ for (const folder of node.folders) walk(folder, path);
82
+ };
83
+ for (const api of tree) walk(api, '');
84
+ return paths;
85
+ }