@w6w/ui 0.0.0-stage → 0.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 (143) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +260 -2
  4. package/dist/ActionTestForm.d.ts +76 -0
  5. package/dist/AddConnectionModal.d.ts +24 -0
  6. package/dist/AppPicker.d.ts +83 -0
  7. package/dist/CodeBlock-CZu4TFSi.cjs +15 -0
  8. package/dist/CodeBlock-CZu4TFSi.cjs.map +1 -0
  9. package/dist/CodeBlock-Dot3Pd7P.js +2889 -0
  10. package/dist/CodeBlock-Dot3Pd7P.js.map +1 -0
  11. package/dist/CodeBlock.d.ts +65 -0
  12. package/dist/CodeEditor.d.ts +42 -0
  13. package/dist/ConfirmModal-DJPYXE9t.cjs +11 -0
  14. package/dist/ConfirmModal-DJPYXE9t.cjs.map +1 -0
  15. package/dist/ConfirmModal-sY1tzkBO.js +6335 -0
  16. package/dist/ConfirmModal-sY1tzkBO.js.map +1 -0
  17. package/dist/Copyable-1niV9gUa.js +771 -0
  18. package/dist/Copyable-1niV9gUa.js.map +1 -0
  19. package/dist/Copyable-CYZU1o4X.cjs +2 -0
  20. package/dist/Copyable-CYZU1o4X.cjs.map +1 -0
  21. package/dist/JsonEditor.d.ts +60 -0
  22. package/dist/NodeConfigForm.d.ts +70 -0
  23. package/dist/ParamsForm.d.ts +56 -0
  24. package/dist/PropertyEntryForm.d.ts +56 -0
  25. package/dist/RetryPolicyFields.d.ts +18 -0
  26. package/dist/StepBuilderModal.d.ts +375 -0
  27. package/dist/TriggerFillForm.d.ts +52 -0
  28. package/dist/WaitEditor.d.ts +6 -0
  29. package/dist/WorkflowFlowEditor.d.ts +119 -0
  30. package/dist/YamlEditor.d.ts +46 -0
  31. package/dist/app-categories.d.ts +38 -0
  32. package/dist/app-pages.d.ts +111 -0
  33. package/dist/code.cjs +2 -0
  34. package/dist/code.cjs.map +1 -0
  35. package/dist/code.d.ts +29 -0
  36. package/dist/code.mjs +7 -0
  37. package/dist/code.mjs.map +1 -0
  38. package/dist/components/ApiCallsPanel.d.ts +41 -0
  39. package/dist/components/AppIcon.d.ts +25 -0
  40. package/dist/components/AuthFieldsForm.d.ts +22 -0
  41. package/dist/components/Combobox.d.ts +24 -0
  42. package/dist/components/ConfirmModal.d.ts +33 -0
  43. package/dist/components/Copyable.d.ts +51 -0
  44. package/dist/components/CopyableText.d.ts +40 -0
  45. package/dist/components/DeleteButton.d.ts +16 -0
  46. package/dist/components/EditButton.d.ts +10 -0
  47. package/dist/components/ExecutionDetail.d.ts +70 -0
  48. package/dist/components/ExecutionFilters.d.ts +59 -0
  49. package/dist/components/ExecutionList.d.ts +60 -0
  50. package/dist/components/ExecutionLogPanel.d.ts +98 -0
  51. package/dist/components/ExecutionStats.d.ts +47 -0
  52. package/dist/components/ExpressionEditorModal.d.ts +42 -0
  53. package/dist/components/ExpressionInput.d.ts +36 -0
  54. package/dist/components/ExpressionOptions.d.ts +185 -0
  55. package/dist/components/HealthStatusPill.d.ts +25 -0
  56. package/dist/components/HistoryTimeline.d.ts +56 -0
  57. package/dist/components/Icon.d.ts +34 -0
  58. package/dist/components/IconButton.d.ts +60 -0
  59. package/dist/components/InfoTip.d.ts +15 -0
  60. package/dist/components/InternalIcon.d.ts +19 -0
  61. package/dist/components/ListItem.d.ts +25 -0
  62. package/dist/components/Modal.d.ts +29 -0
  63. package/dist/components/RepoSyncIndicator.d.ts +29 -0
  64. package/dist/components/ResolvedParams.d.ts +31 -0
  65. package/dist/components/ServerResources.d.ts +47 -0
  66. package/dist/components/StepStatusPill.d.ts +51 -0
  67. package/dist/components/UptimeStrip.d.ts +56 -0
  68. package/dist/components/execution-format.d.ts +24 -0
  69. package/dist/components/expression-dom.d.ts +154 -0
  70. package/dist/components/expression-template.d.ts +86 -0
  71. package/dist/components/history-scale.d.ts +33 -0
  72. package/dist/components/icons.d.ts +41 -0
  73. package/dist/components/resolved-clip.d.ts +42 -0
  74. package/dist/components/server-resources-format.d.ts +68 -0
  75. package/dist/components/server-resources.fixture.d.ts +9 -0
  76. package/dist/components/use-copy.d.ts +14 -0
  77. package/dist/createW6WApi.d.ts +31 -0
  78. package/dist/flow-connect.d.ts +138 -0
  79. package/dist/flow-fanout.d.ts +15 -0
  80. package/dist/flow-types.d.ts +356 -0
  81. package/dist/flow-utils.d.ts +284 -0
  82. package/dist/flow.cjs +3 -0
  83. package/dist/flow.cjs.map +1 -0
  84. package/dist/flow.d.ts +22 -0
  85. package/dist/flow.mjs +1977 -0
  86. package/dist/flow.mjs.map +1 -0
  87. package/dist/index.cjs +2 -0
  88. package/dist/index.cjs.map +1 -0
  89. package/dist/index.d.ts +94 -0
  90. package/dist/index.mjs +1811 -0
  91. package/dist/index.mjs.map +1 -0
  92. package/dist/oauth-popup.d.ts +17 -0
  93. package/dist/output-shape.d.ts +58 -0
  94. package/dist/provider.d.ts +420 -0
  95. package/dist/resolve-params.d.ts +112 -0
  96. package/dist/resolved-json.d.ts +21 -0
  97. package/dist/run-visuals.d.ts +90 -0
  98. package/dist/step-preview-state.d.ts +143 -0
  99. package/dist/theme.d.ts +32 -0
  100. package/dist/trigger-fields.d.ts +56 -0
  101. package/dist/types.d.ts +413 -0
  102. package/dist/ui.css +1 -0
  103. package/dist/use-app-action-defs.d.ts +7 -0
  104. package/dist/use-enter-submit.d.ts +23 -0
  105. package/dist/use-seed-sources.d.ts +16 -0
  106. package/package.json +111 -3
  107. package/src/code.css +542 -0
  108. package/src/styles/_buttons.scss +38 -0
  109. package/src/styles/_chips.scss +210 -0
  110. package/src/styles/_code.scss +139 -0
  111. package/src/styles/_copyable.scss +147 -0
  112. package/src/styles/_data-vars.scss +38 -0
  113. package/src/styles/_execution-history.scss +282 -0
  114. package/src/styles/_execution-log.scss +178 -0
  115. package/src/styles/_expression-field.scss +65 -0
  116. package/src/styles/_expression-inline.scss +63 -0
  117. package/src/styles/_expression-modal.scss +292 -0
  118. package/src/styles/_fields.scss +161 -0
  119. package/src/styles/_flow.scss +217 -0
  120. package/src/styles/_fx.scss +158 -0
  121. package/src/styles/_health.scss +46 -0
  122. package/src/styles/_history-timeline.scss +156 -0
  123. package/src/styles/_icon-button.scss +22 -0
  124. package/src/styles/_icon.scss +15 -0
  125. package/src/styles/_icons.scss +31 -0
  126. package/src/styles/_incoming-state.scss +46 -0
  127. package/src/styles/_info-tip.scss +9 -0
  128. package/src/styles/_layout.scss +6 -0
  129. package/src/styles/_lists.scss +64 -0
  130. package/src/styles/_modal.scss +125 -0
  131. package/src/styles/_multiselect.scss +69 -0
  132. package/src/styles/_repo-sync-indicator.scss +99 -0
  133. package/src/styles/_scale.scss +70 -0
  134. package/src/styles/_sections.scss +36 -0
  135. package/src/styles/_server-resources.scss +163 -0
  136. package/src/styles/_step-builder.scss +376 -0
  137. package/src/styles/_step-config.scss +195 -0
  138. package/src/styles/_tabs.scss +54 -0
  139. package/src/styles/_tester.scss +111 -0
  140. package/src/styles/_tokens.scss +141 -0
  141. package/src/styles/_uptime.scss +61 -0
  142. package/src/styles.css +4208 -0
  143. package/src/styles.scss +61 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Pure formatting + geometry helpers for the server-resources family, kept in
3
+ * their own module so they carry no React and can be unit-tested on their own.
4
+ *
5
+ * `ServerResourceSample` is a thin LOCAL copy of the sampler's wire sample
6
+ * (`plan.md` §Pinned wire) — the same deliberate duplication `UptimeStrip.tsx`
7
+ * keeps of the health union, and for the same reason: it keeps `@w6w/ui` a
8
+ * pure-presentation leaf with no transport/spec coupling, so nothing here has
9
+ * to move when the server's payload grows a field.
10
+ *
11
+ * Every helper is TOTAL: an unknown number is never rendered as `0`, it is
12
+ * rendered as an em dash. A node whose CPU reading is missing must not read as
13
+ * an idle node.
14
+ */
15
+ /** One sample of the host's resource usage, exactly as the server sends it. */
16
+ export interface ServerResourceSample {
17
+ /** ISO-8601 instant the sample was taken. */
18
+ ts: string;
19
+ cpus: number | null;
20
+ cpuLimit: number | null;
21
+ load1: number | null;
22
+ load5: number | null;
23
+ load15: number | null;
24
+ memoryTotal: number | null;
25
+ memoryAvailable: number | null;
26
+ memoryLimit: number | null;
27
+ processRss: number | null;
28
+ processHeapUsed: number | null;
29
+ processHeapTotal: number | null;
30
+ }
31
+ /**
32
+ * A byte count as a short human string: exact bytes below 1 KiB (`1023 B`),
33
+ * one decimal place per binary magnitude from KiB up (`1.5 KiB`, `15.6 GiB`).
34
+ * `null` (nothing measured) is `—`.
35
+ */
36
+ export declare function formatBytes(n: number | null): string;
37
+ /**
38
+ * A 0…1-ish fraction as a whole-percent string (`0.123` → `12%`). Not clamped:
39
+ * a load above the CPU count is real and reads `135%`. `null` is `—`.
40
+ */
41
+ export declare function formatPercent(f: number | null): string;
42
+ /**
43
+ * `load1` against the effective CPU budget — the container's `cpuLimit` when
44
+ * the server set one, otherwise the host CPU count. `null` when either operand
45
+ * is unknown or the divisor is ≤ 0 (a zero-CPU budget is not "0% busy", it is
46
+ * "not measurable").
47
+ */
48
+ export declare function cpuLoadFraction(s: ServerResourceSample | undefined): number | null;
49
+ /**
50
+ * Used memory as a fraction of total: `(memoryTotal − memoryAvailable) /
51
+ * memoryTotal`. `null` when either operand is unknown or the total is ≤ 0.
52
+ */
53
+ export declare function memoryUsedFraction(s: ServerResourceSample | undefined): number | null;
54
+ /**
55
+ * The `points` attribute of a sparkline polyline, hand-drawn — this package
56
+ * has no chart dependency on purpose (D-5), and a polyline over ~60 samples
57
+ * does not justify one.
58
+ *
59
+ * The x axis is positional (`i / (n − 1) · width`), so a gap in the series
60
+ * still advances the line; `y` is inverted (SVG grows downward) and scaled
61
+ * against `max`. Nulls are SKIPPED, not plotted as zero, and a series of fewer
62
+ * than two values has no line at all (`""` — a single point is not a shape).
63
+ * `max ≤ 0` flattens the line along the baseline rather than dividing by it.
64
+ *
65
+ * Every coordinate is rounded to two decimals so the attribute stays short and
66
+ * a snapshot of it stays readable.
67
+ */
68
+ export declare function sparklinePoints(values: readonly (number | null)[], width: number, height: number, max: number): string;
@@ -0,0 +1,9 @@
1
+ import { ServerResourceSample } from './server-resources-format.ts';
2
+ /**
3
+ * `count` samples, oldest first, of a host with 8 CPUs under a 2-CPU cgroup
4
+ * limit and 16 GiB of RAM — so the CPU fraction is a fraction of the LIMIT
5
+ * (load ≈ 0.5 of a 2-CPU budget reads ~25%) and the memory fraction moves
6
+ * around 55%. Every series is non-flat, which is the point: a sparkline over a
7
+ * constant is a straight line and proves nothing in a story.
8
+ */
9
+ export declare function demoServerResourceSamples(count?: number): ServerResourceSample[];
@@ -0,0 +1,14 @@
1
+ /** Majority precedent across the tree: `admin/src/components/CopyButton.tsx:17`,
2
+ * `studio/src/pages/EndpointsPage.tsx:823`. */
3
+ export declare const COPIED_MS = 1500;
4
+ /**
5
+ * The clipboard-write + copied/revert-timer state machine, factored out of
6
+ * `Copyable` so `JsonEditor` can drive its own in-box button without a second
7
+ * clipboard call site. This is the ONLY spot in `@w6w/ui`'s `src` that writes
8
+ * to the clipboard — `Copyable` and `JsonEditor` both consume it rather than
9
+ * each writing their own.
10
+ */
11
+ export declare function useCopyToClipboard(value: string, copiedMs?: number): {
12
+ copied: boolean;
13
+ copy: () => Promise<void>;
14
+ };
@@ -0,0 +1,31 @@
1
+ import { W6WApi } from './provider.tsx';
2
+ export interface CreateW6WApiOptions {
3
+ /** Absolute URL or path prefix — e.g. `"https://w6w.example.com"` or `"/api"`. */
4
+ baseUrl: string;
5
+ /**
6
+ * Bearer token to send. Accepts a string (static) or a supplier function so
7
+ * a JWT that rotates on refresh is fetched fresh on every request. The
8
+ * supplier may be sync or return a `Promise` — either way it is awaited
9
+ * once per request. It receives an optional `{ forceRefresh? }` ctx arg
10
+ * (unused by this client today, but part of the shape so a caller's
11
+ * supplier can share one implementation with `@w6w/sdk`'s `TokenProvider`).
12
+ */
13
+ token?: string | ((ctx?: {
14
+ forceRefresh?: boolean;
15
+ }) => string | null | undefined | Promise<string | null | undefined>);
16
+ /** Optional fetch replacement — handy for tests. Defaults to `globalThis.fetch`. */
17
+ fetch?: typeof fetch;
18
+ }
19
+ export declare class ApiError extends Error {
20
+ status: number;
21
+ code: string;
22
+ /**
23
+ * The parsed error response body, when the server sent one. Carries the
24
+ * fields that ride alongside an error — e.g. an invoke's `logs` and
25
+ * `apiCalls` — which the message alone would drop.
26
+ */
27
+ body?: unknown;
28
+ constructor(status: number, code: string, message: string, body?: unknown);
29
+ }
30
+ /** Build a fetch-based W6WApi client bound to a base URL + token supplier. */
31
+ export declare function createW6WApi(opts: CreateW6WApiOptions): W6WApi;
@@ -0,0 +1,138 @@
1
+ import { Edge } from '@xyflow/react';
2
+ import { PortsLookup } from './flow-types.ts';
3
+ import { StepNode } from './flow-utils.ts';
4
+ /**
5
+ * Which lane a React Flow edge belongs to (core `rfcs/workflow.md` · `Edge.when`
6
+ * amendment). **Omitted ⇒ `"success"`**, and read defensively: an edge that
7
+ * predates the `data.when` stamp — or one React Flow synthesised — may carry no
8
+ * `data` at all. The single accessor: every consumer calls this rather than
9
+ * re-deriving the fallback, so "absent means success" is decided once.
10
+ */
11
+ export declare function edgeLane(e: Edge): "success" | "error";
12
+ /**
13
+ * A refused connection: the source's exit port is full **in this lane** and the cap
14
+ * is finite. Drawing past a finite `out` cap is refused, never satisfied by evicting
15
+ * an existing edge (an unbounded `"many"` out never refuses).
16
+ */
17
+ export type OutCapRefusal = {
18
+ refused: "out_cap";
19
+ cap: number;
20
+ };
21
+ /**
22
+ * The hard rules for an edge `source → target` — the ones no amount of
23
+ * replacement can satisfy: a real, *distinct* pair (no self-loops), no duplicate
24
+ * edge, the target accepts an entry port (blocks connecting *into* a trigger,
25
+ * which declares `in: 0`), and the source has an exit port. Port **capacity** is
26
+ * deliberately NOT checked here: a full single-slot port is freed by replacement
27
+ * (see `applyConnect`), so dragging a new wire from an already-connected node
28
+ * re-points it rather than being rejected. Used as the live `isValidConnection`.
29
+ *
30
+ * The duplicate-pair check is **lane-blind by design**, and that is a v1 *editor*
31
+ * limit, not a model limit: the model permits a success edge and an error edge
32
+ * between the same two steps (D-T1-8 widened the engine's `skippedEdges` key so
33
+ * they cannot collide), but v1 declines to author that pair because there are no
34
+ * id'd source handles to pick the lane from (D-T1-7).
35
+ *
36
+ * A colliding minted **id** is deliberately NOT rejected here, even though
37
+ * {@link applyConnect} refuses it: this predicate is the live `isValidConnection`,
38
+ * and a `false` here means React Flow never fires `onConnect`, so the editor never
39
+ * gets to *say why*. That refusal is invisible (and, worse, lands in
40
+ * `onConnectEnd`'s "dropped on empty canvas" branch, which opens the step builder).
41
+ * Letting the drop through and refusing in `applyConnect` is what lets the reason
42
+ * paint — see {@link connectConflict}.
43
+ */
44
+ export declare function canConnect(source: string | null | undefined, target: string | null | undefined, nodes: StepNode[], edges: Edge[], lookup?: PortsLookup): boolean;
45
+ /**
46
+ * Build the next edge set for a new `source → target` connection. **Additive at the
47
+ * source**: a source under its `out` cap (or unbounded) keeps every existing edge and
48
+ * gains one; at a finite cap the connection is **refused** (see {@link connectRefusal}),
49
+ * never satisfied by evicting. The target's entry port is still single-slot-replacing:
50
+ * the oldest inbound edge(s) are dropped to make room. Returns `null` when the
51
+ * connection is disallowed by {@link canConnect}, refused at the out cap — **or when the minted id would
52
+ * duplicate a surviving edge's id**, the same refusal {@link setEdgeWhen} performs
53
+ * through the same {@link collidingEdgeId} test.
54
+ *
55
+ * That last one closes the reverse order of the `:error` collision: marking `a → b`
56
+ * as the error lane is refused when `a → "b:error"` already exists, but *first*
57
+ * marking `a → b` Error and *then* drawing `a → "b:error"` used to be allowed and
58
+ * minted a duplicate id — React Flow's id-keyed store then held one wire for two
59
+ * edges (measured `storeSize 1 / 2`). Refusing here is narrow on purpose; rejecting
60
+ * the step id itself is the durable fix and would also have to reject ids inside
61
+ * workflows that already exist (FOLLOWUPS.md). The caller must surface the refusal —
62
+ * see {@link connectConflict}.
63
+ *
64
+ * The source's exit capacity is **per lane** (T3.2.2): success edges compete only
65
+ * with success edges, error edges only with error edges. So a finite `out: 1`
66
+ * step refuses a second success edge *within* a lane,
67
+ * while additionally being able to hold one error edge — which is what makes a
68
+ * `send → (error) → fallback` shape authorable at all. No `ports.out` bump is
69
+ * needed, so every persisted definition and palette entry stays valid.
70
+ *
71
+ * The target's entry capacity stays **lane-blind**, and that asymmetry is the
72
+ * point: a converging tail step receiving one success and one error edge is
73
+ * governed by `ports.in` alone. Partitioning the target rule by lane too — the
74
+ * plausible "symmetry" cleanup — would let a `ports.in: 1` node silently accept
75
+ * two inbound edges.
76
+ */
77
+ export declare function applyConnect(source: string | null | undefined, target: string | null | undefined, nodes: StepNode[], edges: Edge[], when?: "success" | "error", lookup?: PortsLookup): Edge[] | null;
78
+ /**
79
+ * The id of the existing edge that drawing `source → target` would collide with, or
80
+ * `null` when the connection is safe (or refused for an ordinary reason — a
81
+ * duplicate pair, a self-loop, a portless step — which the drag already showed the
82
+ * user by marking the drop invalid). The creation-path twin of
83
+ * {@link edgeWhenConflict}, and the same channel: it lets the caller *name* the
84
+ * offending edge in an inline refusal instead of losing a wire silently. The editor
85
+ * selects that edge and renders the message in the edge-lane panel (never a browser
86
+ * dialog; see `.ai/conventions.md`).
87
+ */
88
+ export declare function connectConflict(source: string | null | undefined, target: string | null | undefined, nodes: StepNode[], edges: Edge[], when?: "success" | "error", lookup?: PortsLookup): string | null;
89
+ /**
90
+ * The out-cap refusal for drawing `source → target`, or `null` when the source has
91
+ * room (or is unbounded). The editor renders `cap` in an inline message on the same
92
+ * channel as {@link connectConflict}; {@link applyConnect} returns `null` for it.
93
+ */
94
+ export declare function connectRefusal(source: string | null | undefined, target: string | null | undefined, nodes: StepNode[], edges: Edge[], when?: "success" | "error", lookup?: PortsLookup): OutCapRefusal | null;
95
+ /**
96
+ * Re-lane one edge. Updates its `data.when`, its visuals and its id, and frees
97
+ * nothing: when the destination lane is at a finite out cap the switch is refused
98
+ * (see {@link relaneRefusal}) — the same rule `applyConnect` applies on creation.
99
+ *
100
+ * Returns a **new** array and mutates nothing (React Flow's state handle compares
101
+ * by identity), or **`null` when the switch must be refused** — the `applyConnect`
102
+ * idiom. It is refused when the re-minted id would duplicate a surviving edge's
103
+ * id, which happens for a step id ending in `:error`: `flowEdgeId` qualifies the
104
+ * error lane with that same suffix, so `a → b` in the error lane and `a → "b:error"`
105
+ * in the success lane mint one id, and React Flow's id-keyed store would collapse
106
+ * them — one wire silently vanishing from the canvas, which reads as the editor
107
+ * losing the author's work. The caller must surface the refusal (see
108
+ * {@link edgeWhenConflict}); rejecting the step id itself is the durable fix and
109
+ * is deliberately NOT done here, because it would also have to reject ids inside
110
+ * workflows that already exist (FOLLOWUPS.md).
111
+ *
112
+ * It is refused for one more reason: an `edgeId` owned by more than one edge. That
113
+ * edge set is already corrupt, and re-laning by id would rewrite **every** match,
114
+ * overwriting the other edge's endpoints (`edgeWhenConflict` returns `null` there —
115
+ * there is no *other* edge to name, so the caller's generic refusal applies).
116
+ */
117
+ export declare function setEdgeWhen(edges: Edge[], edgeId: string, when: "success" | "error", nodes: StepNode[], lookup?: PortsLookup): Edge[] | null;
118
+ /**
119
+ * The id of the existing edge that {@link setEdgeWhen} would collide with, or
120
+ * `null` when the switch is safe. Lets the caller *name* the offending edge in an
121
+ * inline refusal instead of failing silently — the editor renders it in the
122
+ * edge-lane panel (never a browser dialog; see `.ai/conventions.md`).
123
+ */
124
+ export declare function edgeWhenConflict(edges: Edge[], edgeId: string, when: "success" | "error", nodes: StepNode[], lookup?: PortsLookup): string | null;
125
+ /** The out-cap refusal for re-laning `edgeId`, or `null` — the re-lane twin of {@link connectRefusal}. */
126
+ export declare function relaneRefusal(edges: Edge[], edgeId: string, when: "success" | "error", nodes: StepNode[], lookup?: PortsLookup): OutCapRefusal | null;
127
+ /**
128
+ * Rewrite the edge set after a step's id changed from `prevId` to `nextId`:
129
+ * re-point every endpoint and **re-mint the id in the edge's own lane**.
130
+ *
131
+ * Extracted from `updateStep` so it is testable, and fixing a real defect: the
132
+ * rewrite used to rebuild every id as `` `${source}->${target}` `` unconditionally,
133
+ * dropping an error edge's `:error` qualifier — so renaming a step collapsed a
134
+ * same-target success+error pair into one edge in React Flow's id-keyed store.
135
+ * That was unreachable while a second outgoing edge deleted the first; per-lane
136
+ * exit capacity is what makes it reachable, hence the fix ships alongside it.
137
+ */
138
+ export declare function renameStepInEdges(edges: Edge[], prevId: string, nextId: string): Edge[];
@@ -0,0 +1,15 @@
1
+ import { Edge } from '@xyflow/react';
2
+ import { FlowStep } from './flow-types.ts';
3
+ export type FanOutMode = "sequential" | "parallel";
4
+ /**
5
+ * `count` is the largest number of outgoing edges the step has in any ONE lane
6
+ * (success / error); fan-out only means something at `count >= 2`.
7
+ */
8
+ export declare function fanOutState(stepId: string, edges: readonly Edge[]): {
9
+ enabled: boolean;
10
+ count: number;
11
+ };
12
+ /** Sequential is the default, so it is expressed by the key's ABSENCE. */
13
+ export declare function setFanOut(step: FlowStep, mode: FanOutMode): FlowStep;
14
+ /** Card-badge wording: how many next steps run, and in which mode. */
15
+ export declare function fanOutLabel(count: number, mode: FanOutMode): string;
@@ -0,0 +1,356 @@
1
+ import { EdgeRunState } from './run-visuals.ts';
2
+ import { ActionParam } from './types.ts';
3
+ export interface FlowStep {
4
+ id: string;
5
+ uses: {
6
+ app: string;
7
+ action: string;
8
+ connection?: string | null;
9
+ };
10
+ /**
11
+ * Declared connection-port cardinality for this step (see core
12
+ * `rfcs/node-types.md` · Ports & cardinality). Omitted fields fall through field-wise to the catalog action, the internal def, then `{ in: 1, out: "many" }`.
13
+ * A persisted value wins over the node's default — `in > 1` opts the step into
14
+ * accepting multiple inbound edges (e.g. a flow-control aggregator joining
15
+ * several upstream branches).
16
+ */
17
+ ports?: PortsDecl;
18
+ with?: Record<string, unknown>;
19
+ retry?: {
20
+ maxAttempts: number;
21
+ backoff?: "fixed" | "exponential";
22
+ delayMs?: number;
23
+ };
24
+ /**
25
+ * What to do when this step errors:
26
+ * - `fail` — stop the run (default)
27
+ * - `continue` — swallow the error and keep going
28
+ * - `continue-record` — keep going, but record the error into the run's end state
29
+ */
30
+ onError?: "fail" | "continue" | "continue-record";
31
+ /** Free-form author notes for this step. Not executed. */
32
+ notes?: string;
33
+ /**
34
+ * How this step's outgoing edges in one lane run when there are two or more:
35
+ * `parallel` runs them concurrently; omitted (never written as `"sequential"`)
36
+ * runs them one after another.
37
+ */
38
+ fanOut?: "sequential" | "parallel";
39
+ /**
40
+ * Authoring-time canvas coordinate for this step, in this editor's own
41
+ * coordinate space (core `rfcs/workflow.md` · "Amendment — 2026-07-29:
42
+ * authoring presentation (`Step.position`, `Workflow.settings`)" — the spec
43
+ * pins no unit, origin or grid). Declarative only: the engine ignores it
44
+ * exactly as it ignores `notes`.
45
+ *
46
+ * **Omitted ⇒ the editor computes a layout** from the graph alone, exactly as
47
+ * before this field existed, so it is additive and backward-compatible.
48
+ * Partial coverage is valid: `workflowToFlow` places the steps that declare a
49
+ * `position` and computes a slot for the rest.
50
+ *
51
+ * Rename-safe by construction — the coordinate travels *with* the step, so
52
+ * there is no workflow-level `id → {x,y}` map to fix up on rename (D-I0-2).
53
+ *
54
+ * Mirrors `Step.position` in `@w6w/workflow-types` by hand — see the header
55
+ * note: these types are deliberately a structural subset with no compiler
56
+ * tie, so the spelling here must stay identical to the engine's.
57
+ */
58
+ position?: {
59
+ x: number;
60
+ y: number;
61
+ };
62
+ }
63
+ export interface FlowEdge {
64
+ from: string;
65
+ to: string;
66
+ /**
67
+ * Which outcome of the `from` step this edge carries (core
68
+ * `rfcs/workflow.md` · "Amendment — 2026-07-29: failure-conditioned edges
69
+ * (`Edge.when`)"). **Omitted ⇒ `"success"`**, so every pre-existing edge is a
70
+ * success edge and needs no migration.
71
+ * - `"success"` — activates when `from` succeeds. Per the house
72
+ * omit-the-default idiom it is written as **absent**, never spelled out:
73
+ * this editor emits no `when` for a success edge, which is what keeps an
74
+ * untouched definition byte-identical across a load/save cycle.
75
+ * - `"error"` — activates when `from` fails, and overrides that step's
76
+ * `onError`.
77
+ *
78
+ * Mirrors `Edge.when` in `@w6w/workflow-types` by hand — see the header note:
79
+ * these types are deliberately a structural subset, with no compiler tie, so
80
+ * the spelling here must stay identical to the engine's.
81
+ */
82
+ when?: "success" | "error";
83
+ }
84
+ /**
85
+ * The React Flow presentation for an edge lane — **the one place** the error
86
+ * look is spelled out.
87
+ *
88
+ * Three callers need it and they run at different times: `workflowToFlow` stamps
89
+ * a *stored* edge on load, `setEdgeWhen` (`flow-connect.ts`) stamps an edge the
90
+ * author re-lanes **live**, before any save, and a live-run repaint effect
91
+ * (`WorkflowFlowEditor.tsx`, T1.1.1) restyles an edge as a run polls in. Spelled
92
+ * out more than once, the copies drift and a freshly marked error edge stops
93
+ * looking like a reloaded one. The className is what `styles.css`'s
94
+ * `.w6w-edge-error`/`.w6w-edge-taken`/`.w6w-edge-skipped` blocks paint from
95
+ * `--w6w-danger`/`--w6w-success`/`--w6w-muted`; no custom edge component is
96
+ * involved (D-T1-7).
97
+ *
98
+ * `runState` (T1.1.1) is the edge's OWN taken/skipped verdict (see
99
+ * `run-visuals.ts`'s `edgeRunState`) — omitted (or `undefined`) when there is no
100
+ * run in progress, or the run hasn't decided this edge's fate yet, in which case
101
+ * this returns exactly what it always has. `workflowToFlow`'s and `setEdgeWhen`'s
102
+ * call sites deliberately never have a live run's state available (a *stored*
103
+ * load and a live *re-lane* are never mid-run), so both pass no `runState` at
104
+ * all — only the new repaint-effect call site does.
105
+ *
106
+ * A `"success"`/absent lane with no `runState` returns an **empty object** — not
107
+ * `{ className: "" }` — so an untouched success edge carries neither key and a
108
+ * definition round-trips unchanged. That makes it a trap for a *re-laning*
109
+ * caller: spreading `{}` over an edge that already carries the error class
110
+ * leaves the class in place, so `setEdgeWhen` assigns both keys explicitly
111
+ * rather than spreading.
112
+ */
113
+ export declare function edgeVisuals(when: "success" | "error" | undefined, runState?: EdgeRunState): {
114
+ className?: string;
115
+ label?: string;
116
+ };
117
+ /**
118
+ * The React Flow handle id a step/control node's **error exit port** carries
119
+ * (T1.1.1). Internal to `@w6w/ui` — not re-exported from `flow.ts` / `index.ts`
120
+ * (D-P2), since no consumer outside this package needs it.
121
+ */
122
+ export declare const ERROR_SOURCE_HANDLE = "out-error";
123
+ /**
124
+ * The `sourceHandle` a minted/re-laned/loaded edge carries for a given lane —
125
+ * the inverse of {@link laneForSourceHandle}. `undefined` for `"success"`/
126
+ * absent is **load-bearing, not stylistic**: React Flow anchors a handle-less
127
+ * edge to the node's sole *unnamed* handle, which is the exit every
128
+ * already-saved workflow's edges implicitly use. An empty string would not do
129
+ * this — an empty string is a real, if degenerate, handle id.
130
+ */
131
+ export declare function sourceHandleForLane(when: "success" | "error" | undefined): string | undefined;
132
+ /**
133
+ * The lane a drag started from, read off the handle id React Flow reports.
134
+ * **The one canonicalisation point** every minting/re-laning site calls
135
+ * (`onConnect`, `onConnectEnd`) so the handle-id spelling is decided once and
136
+ * both drag gestures provably agree — two inline comparisons is the shape
137
+ * where one gesture silently drifts. Exactly {@link ERROR_SOURCE_HANDLE} is
138
+ * `"error"`; everything else — a near-miss id, `null`, `undefined` — is
139
+ * `"success"`.
140
+ */
141
+ export declare function laneForSourceHandle(handleId: string | null | undefined): "success" | "error";
142
+ export interface FlowWorkflow {
143
+ manifestVersion: string;
144
+ id: string;
145
+ name: string;
146
+ displayName?: string;
147
+ description?: string;
148
+ variables?: Array<{
149
+ key: string;
150
+ type?: string;
151
+ required?: boolean;
152
+ default?: unknown;
153
+ }>;
154
+ steps: FlowStep[];
155
+ edges?: FlowEdge[];
156
+ /**
157
+ * Authoring-time presentation and preferences for this workflow (core
158
+ * `rfcs/workflow.md` · "Amendment — 2026-07-29: authoring presentation
159
+ * (`Step.position`, `Workflow.settings`)"). Declarative only — the engine
160
+ * ignores it, exactly like `FlowStep.notes` and `FlowStep.position`.
161
+ * `viewport` sits *inside* `settings` deliberately, so exactly one new
162
+ * top-level key enters the portable workflow document. `settings` is itself
163
+ * optional; when the object is absent every member below still takes its own
164
+ * default.
165
+ *
166
+ * ⚠️ **`autoSave` and `savePosition` default to `true` when omitted.** That is
167
+ * *deliberately unlike* the `ports`/`onError` rule ("omitted reproduces the
168
+ * status quo") — here it does not. Read them as `?? true` / `!== false`, never
169
+ * `?? false` and never `=== true`. The consequence, stated plainly: **a
170
+ * workflow that has never been saved by the new editor starts persisting step
171
+ * coordinates the first time it is saved.** That is intended (the product
172
+ * requires both features on by default); only an explicit `false` turns one
173
+ * off.
174
+ *
175
+ * HITL-6 asked whether a workflow's arrangement and its auto-save preference
176
+ * belong to *the workflow* or to *the person looking at it*; the amendment
177
+ * takes the **workflow** answer, so everyone who opens a workflow sees the
178
+ * same arrangement. HITL-6 is still open — a viewer-owned answer would move
179
+ * these fields out of the document and re-contract this shape.
180
+ *
181
+ * Mirrors `Workflow.settings` in `@w6w/workflow-types` by hand (structural
182
+ * subset, no compiler tie — the spelling must stay identical).
183
+ */
184
+ settings?: {
185
+ /**
186
+ * Whether an authoring tool persists edits without an explicit save action.
187
+ * Omitted ⇒ **true** — auto-save is ON. Not read by this module; the editor
188
+ * and its host own it (T3.3.2 / T4.1.x).
189
+ */
190
+ autoSave?: boolean;
191
+ /**
192
+ * Whether an authoring tool persists step coordinates (`FlowStep.position`)
193
+ * and `settings.viewport` when it saves. Omitted ⇒ **true** — positions ARE
194
+ * persisted. When explicitly `false`, `flowToWorkflow` writes no `position`;
195
+ * any value already stored is **left as it is, never erased**.
196
+ */
197
+ savePosition?: boolean;
198
+ /**
199
+ * The camera position the author last left the canvas at, so reopening the
200
+ * workflow restores the view. **No default** — a workflow with no `viewport`
201
+ * opens at whatever view the editor computes, exactly as today. Written by
202
+ * the component (T3.3.2), not by `flowToWorkflow`, whose signature cannot
203
+ * see a viewport.
204
+ */
205
+ viewport?: {
206
+ x: number;
207
+ y: number;
208
+ zoom: number;
209
+ };
210
+ };
211
+ }
212
+ /** Reserved namespace for internal pseudo-apps. */
213
+ export declare const INTERNAL_APP_PREFIX = "@w6w/";
214
+ /** Engine-native flow control (if/foreach/parallel/wait). */
215
+ export declare const CONTROL_APP = "@w6w/control";
216
+ /** Host-run inline JS. */
217
+ export declare const SCRIPT_APP = "@w6w/script";
218
+ /** Host-run typed key/value data. */
219
+ export declare const DATA_APP = "@w6w/data";
220
+ /** The workflow's entry/trigger node. */
221
+ export declare const TRIGGER_APP = "@w6w/trigger";
222
+ /** Host-run outbound HTTP(S) request. */
223
+ export declare const HTTP_APP = "@w6w/http";
224
+ /** Inbound HTTP(S) webhook trigger (entry node; provisions a receive URL). */
225
+ export declare const WEBHOOK_APP = "@w6w/webhook";
226
+ /** Time-based scheduler trigger (entry node; fires the workflow on a schedule). */
227
+ export declare const SCHEDULER_APP = "@w6w/scheduler";
228
+ /** "Respond to Webhook" — shapes the HTTP response for `responseMode: responseNode`. */
229
+ export declare const RESPOND_APP = "@w6w/respond";
230
+ /**
231
+ * Sub-workflow / Function caller (core rfcs/node-types.md · F-3). The host runs
232
+ * this node itself: it resolves `with.target` to a project-scoped `CallableRef` (a
233
+ * Function or a Workflow) and invokes it through `ctx.invokeCallable`, honoring
234
+ * the per-node `with.wait` flag. The engine never loads the target.
235
+ */
236
+ export declare const CALL_APP = "@w6w/call";
237
+ /**
238
+ * Read a stored JSON document by key (core rfcs/node-types.md · F-3 amendment,
239
+ * "Reserved internal pseudo-app"). Host node: `runInternalNode` short-circuits
240
+ * this id before the registry loads a module — the run's own project is bound
241
+ * host-side, so there is no `project` param (C-6).
242
+ */
243
+ export declare const DOCUMENT_APP = "@w6w/document";
244
+ /**
245
+ * Compile a Handlebars `template` string against a resolved `values` object
246
+ * (core rfcs/node-types.md · F-3 amendment, "the `@w6w/template` host node").
247
+ * Host node: `runInternalNode` short-circuits this id before the registry
248
+ * loads a module — it needs no privileged host resource (no DB, no
249
+ * tenant/project scope), and runs host-side only because the template
250
+ * compiles to a JS function, the same risk shape `@w6w/script` has.
251
+ */
252
+ export declare const TEMPLATE_APP = "@w6w/template";
253
+ /** True when `app` is a reserved internal pseudo-app id (`@w6w/*`). */
254
+ export declare function isInternalApp(app: string): boolean;
255
+ /** True when a node is an engine-native flow-control node (can't run standalone). */
256
+ export declare function isControlApp(app: string): boolean;
257
+ /**
258
+ * Whether a step is an entry/trigger node — the one predicate, shared by the
259
+ * step editor's Test tab and the canvas ▶ collect phase. A trigger's configured
260
+ * `fields` are *definitions*, so both surfaces project them into a fillable form
261
+ * and send the filled values as `{ input }` rather than running the raw config.
262
+ */
263
+ export declare function isTriggerApp(app: string): boolean;
264
+ /** A palette entry for an internal node: its id, label, group, and config schema. */
265
+ export interface InternalNodeDef {
266
+ app: string;
267
+ action: string;
268
+ label: string;
269
+ /**
270
+ * Human display name (mirrors an app's `displayName`). Defaults conceptually
271
+ * to `label`; kept explicit so internal pseudo-apps carry the same info an app
272
+ * does. Shown wherever an app's name would be.
273
+ */
274
+ displayName: string;
275
+ group: "trigger" | "control" | "compute" | "request";
276
+ /**
277
+ * Inline SVG *inner* markup (paths / circles / polylines) for this primitive's
278
+ * glyph — internal pseudo-apps have no icon asset dir, so the glyph is bundled
279
+ * here. Drawn on a 24×24 `viewBox`, stroked with `currentColor` (theme-aware),
280
+ * so both app nodes and internal nodes display a consistent icon on the canvas.
281
+ */
282
+ icon: string;
283
+ /**
284
+ * Hidden from every palette list but still resolvable by `internalNodeDef`, so
285
+ * a workflow already using the node keeps rendering and editing.
286
+ */
287
+ hidden?: boolean;
288
+ /**
289
+ * Connection ports: how many inbound (entry) and outbound (exit) connections
290
+ * this node accepts. A port is the ability to receive/emit a connection —
291
+ * rendered as a React Flow Handle. Defaults to one of each (`{ in: 1, out: 1 }`)
292
+ * when omitted; a trigger overrides to `{ in: 0, out: 1 }` (nothing flows into
293
+ * the entry node). Fixed for now — not user-editable.
294
+ */
295
+ ports?: PortsDecl;
296
+ /** Config schema (same `ActionParam[]` shape apps declare) rendered by ParamsForm. */
297
+ params: ActionParam[];
298
+ /**
299
+ * This node's static output field keys, when its shape is known ahead of any
300
+ * run — same shape as `ExpressionStepSource.outputs`
301
+ * (`components/ExpressionOptions.tsx`), fed verbatim by `projectStepSources`
302
+ * (`step-preview-state.ts`) so the expression rail can offer
303
+ * `steps.<id>.output.<key>` for a step using this node. Omitted (not `[]`)
304
+ * when the node's output is not statically knowable (e.g. `@w6w/document ·
305
+ * get`, whose shape is whatever JSON the referenced document holds).
306
+ */
307
+ output?: {
308
+ key: string;
309
+ label?: string;
310
+ }[];
311
+ }
312
+ /** A declared port count: a number, or `"many"` (unbounded). */
313
+ export type PortCount = number | "many";
314
+ /** A (possibly partial) port declaration, as an author / catalog / def writes it. */
315
+ export interface PortsDecl {
316
+ in?: PortCount;
317
+ out?: PortCount;
318
+ }
319
+ /** Resolved inbound (entry) and outbound (exit) connection-port counts; `"many"` is `Infinity`. */
320
+ export interface NodePorts {
321
+ in: number;
322
+ out: number;
323
+ }
324
+ /** The default a node gets when nothing declares ports: 1 in, unbounded out. */
325
+ export declare const DEFAULT_NODE_PORTS: NodePorts;
326
+ /** Field-wise resolution: first declared value per field wins, else the default. */
327
+ export declare function resolvePorts(...layers: (PortsDecl | undefined)[]): NodePorts;
328
+ /** Catalog lookup of an action's declared ports, passed as an argument (never stamped on a step). */
329
+ export type PortsLookup = (app: string, action: string) => PortsDecl | undefined;
330
+ /**
331
+ * Resolve a node's connection ports from its internal def only (triggers drop the
332
+ * entry port); everything else gets the default.
333
+ */
334
+ export declare function nodePorts(app: string, action: string): NodePorts;
335
+ /**
336
+ * Resolve a *step's* ports field-wise: `step.ports` → catalog action (via `lookup`)
337
+ * → internal def → default `{ in: 1, out: Infinity }`. The catalog is read through
338
+ * the `lookup` argument and never written onto the step, so it never serializes.
339
+ */
340
+ export declare function nodePortsForStep(step: FlowStep, lookup?: PortsLookup): NodePorts;
341
+ /** The built-in internal nodes, in palette order. */
342
+ export declare const INTERNAL_NODES: InternalNodeDef[];
343
+ /** Look up an internal node's definition by its (app, action) pair. */
344
+ export declare function internalNodeDef(app: string, action: string): InternalNodeDef | undefined;
345
+ /** The label the editor shows for an internal node (falls back to the action key). */
346
+ export declare function internalNodeLabel(app: string, action: string): string;
347
+ /**
348
+ * The inline SVG glyph markup for an internal node (empty when unknown). Lets a
349
+ * node retrieve its icon from just the (app, action) pair — same lookup path as
350
+ * `internalNodeLabel`.
351
+ */
352
+ export declare function internalNodeIcon(app: string, action: string): string;
353
+ /** The config schema for an internal node (empty when unknown). */
354
+ export declare function internalNodeParams(app: string, action: string): ActionParam[];
355
+ /** Build an internal node's default `with` from its param schema defaults. */
356
+ export declare function internalNodeDefaults(app: string, action: string): Record<string, unknown>;