foldkit 0.99.0 → 0.100.1

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 (45) hide show
  1. package/README.md +37 -35
  2. package/dist/devTools/overlay.d.ts.map +1 -1
  3. package/dist/devTools/overlay.js +6 -1
  4. package/dist/devTools/protocol.d.ts +94 -2
  5. package/dist/devTools/protocol.d.ts.map +1 -1
  6. package/dist/devTools/protocol.js +32 -0
  7. package/dist/devTools/public.d.ts +1 -1
  8. package/dist/devTools/public.d.ts.map +1 -1
  9. package/dist/devTools/public.js +1 -1
  10. package/dist/devTools/schemaSummarize.d.ts +82 -0
  11. package/dist/devTools/schemaSummarize.d.ts.map +1 -0
  12. package/dist/devTools/schemaSummarize.js +264 -0
  13. package/dist/devTools/store.d.ts.map +1 -1
  14. package/dist/devTools/store.js +50 -49
  15. package/dist/devTools/webSocketBridge.d.ts +9 -0
  16. package/dist/devTools/webSocketBridge.d.ts.map +1 -1
  17. package/dist/devTools/webSocketBridge.js +64 -3
  18. package/dist/runtime/browserListeners.d.ts +1 -0
  19. package/dist/runtime/browserListeners.d.ts.map +1 -1
  20. package/dist/runtime/browserListeners.js +17 -5
  21. package/dist/test/apps/bubbling.d.ts.map +1 -1
  22. package/dist/test/apps/bubbling.js +7 -5
  23. package/dist/test/apps/counter.d.ts.map +1 -1
  24. package/dist/test/apps/counter.js +8 -6
  25. package/dist/test/apps/disabledButton.d.ts.map +1 -1
  26. package/dist/test/apps/disabledButton.js +32 -21
  27. package/dist/test/apps/fileUpload.d.ts.map +1 -1
  28. package/dist/test/apps/fileUpload.js +18 -16
  29. package/dist/test/apps/interactions.d.ts.map +1 -1
  30. package/dist/test/apps/interactions.js +21 -19
  31. package/dist/test/apps/keypress.d.ts.map +1 -1
  32. package/dist/test/apps/keypress.js +12 -10
  33. package/dist/test/apps/login.d.ts.map +1 -1
  34. package/dist/test/apps/login.js +38 -36
  35. package/dist/test/apps/logoutButton.d.ts.map +1 -1
  36. package/dist/test/apps/logoutButton.js +4 -2
  37. package/dist/test/apps/mountPanel.d.ts.map +1 -1
  38. package/dist/test/apps/mountPanel.js +29 -21
  39. package/dist/test/apps/multiRole.d.ts.map +1 -1
  40. package/dist/test/apps/multiRole.js +6 -4
  41. package/dist/test/apps/pointer.d.ts.map +1 -1
  42. package/dist/test/apps/pointer.js +15 -13
  43. package/dist/test/apps/resumeUpload.d.ts.map +1 -1
  44. package/dist/test/apps/resumeUpload.js +20 -15
  45. package/package.json +1 -1
@@ -0,0 +1,264 @@
1
+ import { Array as Array_, Option, Predicate, Record, String as String_, flow, pipe, } from 'effect';
2
+ const PATH_SEPARATOR = '.';
3
+ const isRecord = (value) => Predicate.isObject(value) && !Array_.isArray(value);
4
+ const isReadonlyArray = (value) => Array_.isArray(value);
5
+ const isStringArray = (values) => Array_.every(values, Predicate.isString);
6
+ const stringEnumOf = (schema) => {
7
+ if (!isRecord(schema)) {
8
+ return Option.none();
9
+ }
10
+ const candidate = schema['enum'];
11
+ if (!Array_.isArray(candidate)) {
12
+ return Option.none();
13
+ }
14
+ return Option.liftPredicate(candidate, isStringArray);
15
+ };
16
+ const variantTagOf = (schema) => {
17
+ if (!isRecord(schema) || schema['type'] !== 'object') {
18
+ return Option.none();
19
+ }
20
+ const properties = schema['properties'];
21
+ if (!isRecord(properties)) {
22
+ return Option.none();
23
+ }
24
+ return stringEnumOf(properties['_tag']).pipe(Option.flatMap(tags => tags.length === 1 ? Array_.head(tags) : Option.none()));
25
+ };
26
+ const anyOfOf = (schema) => {
27
+ if (!isRecord(schema)) {
28
+ return Option.none();
29
+ }
30
+ return Option.liftPredicate(schema['anyOf'], isReadonlyArray);
31
+ };
32
+ const isDiscriminatedUnion = (schema) => Option.exists(anyOfOf(schema), Predicate.and(Array_.isReadonlyArrayNonEmpty, Array_.every(flow(variantTagOf, Option.isSome))));
33
+ const payloadFieldsOf = (variant) => {
34
+ if (!isRecord(variant)) {
35
+ return [];
36
+ }
37
+ const properties = variant['properties'];
38
+ if (!isRecord(properties)) {
39
+ return [];
40
+ }
41
+ return pipe(Record.keys(properties), Array_.filter(name => name !== '_tag'));
42
+ };
43
+ const unionFieldsOf = (variant) => {
44
+ if (!isRecord(variant)) {
45
+ return [];
46
+ }
47
+ const properties = variant['properties'];
48
+ if (!isRecord(properties)) {
49
+ return [];
50
+ }
51
+ return pipe(Record.toEntries(properties), Array_.filter(([name, schema]) => name !== '_tag' && isDiscriminatedUnion(schema)), Array_.map(([name]) => name));
52
+ };
53
+ const entryOf = (variant) => variantTagOf(variant).pipe(Option.map(tag => ({
54
+ tag,
55
+ payloadFields: payloadFieldsOf(variant),
56
+ unionFields: unionFieldsOf(variant),
57
+ })));
58
+ const topLevelVariantsOf = (document) => {
59
+ if (!isRecord(document)) {
60
+ return Option.none();
61
+ }
62
+ return anyOfOf(document['schema']);
63
+ };
64
+ const indexEntriesOf = (members) => pipe(members, Array_.map(entryOf), Array_.getSomes);
65
+ /**
66
+ * Build a flat directory of every top-level Message variant from a JSON Schema
67
+ * document produced by `Schema.toJsonSchemaDocument`. The directory is small
68
+ * even for hundreds of variants, so an MCP client can paginate by tag without
69
+ * paying for the full schema.
70
+ *
71
+ * Returns `None` when the document's top-level `schema` is not a discriminated
72
+ * union of `_tag`-keyed structs (e.g. a single-variant Message Schema, or a
73
+ * shape produced by a future Effect Schema release that the summarizer does
74
+ * not yet understand). The caller should fall back to fetching the full
75
+ * document in that case.
76
+ */
77
+ export const indexMessageSchemaDocument = (document) => Option.map(topLevelVariantsOf(document), indexEntriesOf);
78
+ const collapseUnionsInValue = (value) => {
79
+ if (Array_.isArray(value)) {
80
+ return Array_.map(value, collapseUnionsInValue);
81
+ }
82
+ if (!isRecord(value)) {
83
+ return value;
84
+ }
85
+ if (isDiscriminatedUnion(value)) {
86
+ return Option.match(anyOfOf(value), {
87
+ onNone: () => value,
88
+ onSome: members => ({
89
+ _summary: 'union',
90
+ variants: indexEntriesOf(members),
91
+ }),
92
+ });
93
+ }
94
+ return Record.map(value, child => collapseUnionsInValue(child));
95
+ };
96
+ const collapseUnionsInVariantPayload = (variant) => {
97
+ if (!isRecord(variant)) {
98
+ return variant;
99
+ }
100
+ const properties = variant['properties'];
101
+ if (!isRecord(properties)) {
102
+ return variant;
103
+ }
104
+ const collapsed = Record.map(properties, (schema, name) => name === '_tag' ? schema : collapseUnionsInValue(schema));
105
+ return { ...variant, properties: collapsed };
106
+ };
107
+ const findVariantByTag = (members, tag) => Array_.findFirst(members, variant => Option.exists(variantTagOf(variant), candidate => candidate === tag));
108
+ /**
109
+ * Idiomatic Foldkit Messages carry at most one tagged-union payload field per
110
+ * variant: either a `Got<Child>Message { message }` Submodel wrapper or a
111
+ * regular Message with one tagged-union value-type payload (e.g. `ClickedLink {
112
+ * request: UrlRequest }`). Multi-union-field variants are non-idiomatic;
113
+ * surrounding state that a child Submodel needs belongs as an argument to
114
+ * the child's `update`/`view`, not as a sibling field on the parent Message.
115
+ * Returns `None` when zero or multiple union fields exist so the path walker
116
+ * can produce an actionable error rather than silently picking one.
117
+ */
118
+ const singleUnionFieldOf = (variant) => {
119
+ if (!isRecord(variant)) {
120
+ return Option.none();
121
+ }
122
+ const properties = variant['properties'];
123
+ if (!isRecord(properties)) {
124
+ return Option.none();
125
+ }
126
+ const unionEntries = pipe(Record.toEntries(properties), Array_.filter(([name, schema]) => name !== '_tag' && isDiscriminatedUnion(schema)));
127
+ if (unionEntries.length !== 1) {
128
+ return Option.none();
129
+ }
130
+ return Option.gen(function* () {
131
+ const [name, schema] = yield* Array_.head(unionEntries);
132
+ const members = yield* anyOfOf(schema);
133
+ return { name, members };
134
+ });
135
+ };
136
+ const replaceUnionField = (variant, fieldName, narrowedChild) => {
137
+ if (!isRecord(variant)) {
138
+ return variant;
139
+ }
140
+ const properties = variant['properties'];
141
+ if (!isRecord(properties)) {
142
+ return variant;
143
+ }
144
+ const updated = {
145
+ ...properties,
146
+ [fieldName]: { anyOf: [narrowedChild] },
147
+ };
148
+ return { ...variant, properties: updated };
149
+ };
150
+ const stepIntoVariant = (variant, rest) => Option.gen(function* () {
151
+ const field = yield* singleUnionFieldOf(variant);
152
+ const narrowedChild = yield* narrowAtPath(field.members, rest);
153
+ return replaceUnionField(variant, field.name, narrowedChild);
154
+ });
155
+ const narrowAtPath = (members, segments) => Option.gen(function* () {
156
+ const tag = yield* Array_.head(segments);
157
+ const variant = yield* findVariantByTag(members, tag);
158
+ const rest = Array_.drop(segments, 1);
159
+ if (Array_.isReadonlyArrayEmpty(rest)) {
160
+ return collapseUnionsInVariantPayload(variant);
161
+ }
162
+ return yield* stepIntoVariant(variant, rest);
163
+ });
164
+ /**
165
+ * Split a dot-separated variant path into its segments. Empty segments are
166
+ * dropped so callers can pass user-supplied strings without first trimming
167
+ * leading/trailing dots.
168
+ */
169
+ export const splitVariantPath = (variantPath) => pipe(variantPath, String_.split(PATH_SEPARATOR), Array_.filter(String_.isNonEmpty));
170
+ const stepToNextUnionMembers = (members, segment) => Option.gen(function* () {
171
+ const variant = yield* findVariantByTag(members, segment);
172
+ const field = yield* singleUnionFieldOf(variant);
173
+ return field.members;
174
+ });
175
+ const variantsAtPathPrefix = (document, segments) => Option.flatMap(topLevelVariantsOf(document), topMembers => Array_.reduce(segments, Option.some(topMembers), (currentMembers, segment) => Option.flatMap(currentMembers, members => stepToNextUnionMembers(members, segment))));
176
+ /**
177
+ * Enumerate the variant tags available as the next segment of a variant path.
178
+ * Given a partial path that resolves to a tagged-union field, returns the tags
179
+ * of every variant in that union. Useful for crafting `not-found` error
180
+ * messages: if `narrowToVariant` fails on `"a.b.c"`, calling this with `["a", "b"]`
181
+ * yields the valid choices for the third segment.
182
+ *
183
+ * Returns `None` when the prefix cannot be resolved at all (e.g. the first
184
+ * segment names no top-level variant, or an intermediate variant lacks a
185
+ * tagged-union payload field).
186
+ */
187
+ export const variantTagsAtPathPrefix = (document, pathPrefix) => Option.map(variantsAtPathPrefix(document, pathPrefix), members => Array_.map(indexEntriesOf(members), entry => entry.tag));
188
+ /**
189
+ * Diagnose where a variant-path walk would fail. Walks back from one segment
190
+ * before the supplied path length, returning the deepest prefix whose
191
+ * tagged-union level resolves cleanly, the tags available at that level, and
192
+ * the next segment from the original path (the one that broke the walk). When
193
+ * the offending segment is a known tag at that level, the failure means the
194
+ * variant exists but has no tagged-union field to step into further. When it
195
+ * is unknown, the failure is a simple typo. Returns `None` when not even the
196
+ * empty prefix resolves (i.e. the document is not a discriminated union at
197
+ * the top level).
198
+ */
199
+ export const diagnoseVariantPath = (document, segments) => {
200
+ const tryLength = (length) => {
201
+ if (length < 0) {
202
+ return Option.none();
203
+ }
204
+ const prefix = Array_.take(segments, length);
205
+ return Option.match(variantTagsAtPathPrefix(document, prefix), {
206
+ onNone: () => tryLength(length - 1),
207
+ onSome: available => Option.some({
208
+ prefix,
209
+ failingSegment: Array_.get(segments, length),
210
+ available,
211
+ }),
212
+ });
213
+ };
214
+ return tryLength(segments.length - 1);
215
+ };
216
+ /**
217
+ * Replace the top-level `anyOf` in a JSON Schema document with a single-element
218
+ * `anyOf` containing the variant(s) selected by a dot-separated variant path.
219
+ *
220
+ * `variantPath` is a dot-string of variant `_tag` values, walked through the
221
+ * one tagged-union payload field at each step. For example, `"GotChildMessage"`
222
+ * narrows the top-level union to that wrapper variant, and
223
+ * `"GotChildMessage.Opened"` walks into the wrapper's nested union and narrows
224
+ * to the inner variant. Any deeper discriminated unions inside the deepest
225
+ * variant's payload are collapsed to `{ _summary: 'union', variants: [...] }`
226
+ * placeholders so the response stays small even for deeply-nested Submodel
227
+ * trees; agents drill further by extending the path.
228
+ *
229
+ * The `definitions` block is kept (any `$ref` targets the narrowed variant
230
+ * relies on still resolve, and dead refs left over from trimmed variants are
231
+ * harmless), but any discriminated unions inside it are collapsed to the same
232
+ * `_summary` placeholder shape so a shared union annotated with an
233
+ * `identifier` does not balloon the response. The path walker does not
234
+ * resolve `$ref` indirection through `definitions`; agents that need to step
235
+ * through a `$ref`-shared union look up the definition by name and use the
236
+ * placeholder's variant list directly.
237
+ *
238
+ * Returns `None` when the document is not a top-level discriminated union, the
239
+ * path is empty, a segment names no variant in the current union, or an
240
+ * intermediate variant lacks exactly one tagged-union payload field to step
241
+ * into (zero or multiple union fields are both ambiguous). The "exactly one"
242
+ * rule encodes the Foldkit idiom (`Got<Child>Message { message }` Submodel
243
+ * wrappers, single-union value-type fields); apps whose Message variants need
244
+ * additional surrounding state should pass it as an argument to the child's
245
+ * `update`/`view` rather than as a sibling field on the parent Message.
246
+ */
247
+ export const narrowToVariant = (document, variantPath) => {
248
+ if (!isRecord(document)) {
249
+ return Option.none();
250
+ }
251
+ const segments = splitVariantPath(variantPath);
252
+ if (Array_.isReadonlyArrayEmpty(segments)) {
253
+ return Option.none();
254
+ }
255
+ return Option.gen(function* () {
256
+ const members = yield* topLevelVariantsOf(document);
257
+ const narrowed = yield* narrowAtPath(members, segments);
258
+ return {
259
+ ...document,
260
+ schema: { anyOf: [narrowed] },
261
+ definitions: collapseUnionsInValue(document['definitions']),
262
+ };
263
+ });
264
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/devTools/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/devTools/store.ts"],"names":[],"mappings":"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"}
@@ -97,28 +97,26 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
97
97
  : state.startIndex;
98
98
  return pipe(state.keyframes, HashMap.get(keyframeIndex), Option.map(keyframeModel => pipe(state.entries, Array.drop(keyframeIndex - state.startIndex), Array.take(index - keyframeIndex + 1), Array.reduce(keyframeModel, (model, entry) => bridge.replay(model, entry.message)))), Option.getOrThrow);
99
99
  };
100
- const addKeyframeIfNeeded = (keyframes, nextAbsoluteIndex, modelAfterUpdate) => nextAbsoluteIndex % keyframeInterval === 0
100
+ const addKeyframeIfNeeded = (nextAbsoluteIndex, modelAfterUpdate) => (keyframes) => nextAbsoluteIndex % keyframeInterval === 0
101
101
  ? HashMap.set(keyframes, nextAbsoluteIndex, modelAfterUpdate)
102
102
  : keyframes;
103
103
  const evictOldestSegment = (state) => {
104
104
  const nextStartIndex = state.startIndex + keyframeInterval;
105
105
  const isPausedAtRetainedIndex = state.pausedAtIndex >= nextStartIndex ||
106
106
  state.pausedAtIndex === INIT_INDEX;
107
- return {
108
- ...state,
109
- entries: Array.drop(state.entries, keyframeInterval),
110
- keyframes: HashMap.remove(state.keyframes, state.startIndex),
111
- startIndex: nextStartIndex,
112
- isPaused: state.isPaused && isPausedAtRetainedIndex,
113
- };
107
+ return evo(state, {
108
+ entries: Array.drop(keyframeInterval),
109
+ keyframes: HashMap.remove(state.startIndex),
110
+ startIndex: () => nextStartIndex,
111
+ isPaused: isPaused => isPaused && isPausedAtRetainedIndex,
112
+ });
114
113
  };
115
- const recordInit = (model, commands, mountStarts = []) => SubscriptionRef.update(stateRef, state => ({
116
- ...state,
117
- maybeInitModel: Option.some(model),
118
- initCommands: commands,
119
- initMountStarts: mountStarts,
120
- keyframes: HashMap.set(state.keyframes, 0, model),
121
- maybeLatestModel: Option.some(model),
114
+ const recordInit = (model, commands, mountStarts = []) => SubscriptionRef.update(stateRef, state => evo(state, {
115
+ maybeInitModel: () => Option.some(model),
116
+ initCommands: () => commands,
117
+ initMountStarts: () => mountStarts,
118
+ keyframes: HashMap.set(0, model),
119
+ maybeLatestModel: () => Option.some(model),
122
120
  }));
123
121
  const recordMessage = (message, modelBeforeUpdate, modelAfterUpdate, commands, isModelChanged) => SubscriptionRef.update(stateRef, state => {
124
122
  const absoluteIndex = state.startIndex + state.entries.length;
@@ -126,9 +124,8 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
126
124
  ? computeDiff(modelBeforeUpdate, modelAfterUpdate)
127
125
  : emptyDiff;
128
126
  const hasChangedFields = HashSet.size(diff.changedPaths) > 0;
129
- const nextState = {
130
- ...state,
131
- entries: Array.append(state.entries, {
127
+ const nextState = evo(state, {
128
+ entries: Array.append({
132
129
  tag: message._tag,
133
130
  message,
134
131
  commands,
@@ -138,9 +135,9 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
138
135
  isModelChanged: hasChangedFields,
139
136
  diff,
140
137
  }),
141
- keyframes: addKeyframeIfNeeded(state.keyframes, absoluteIndex + 1, modelAfterUpdate),
142
- maybeLatestModel: Option.some(modelAfterUpdate),
143
- };
138
+ keyframes: addKeyframeIfNeeded(absoluteIndex + 1, modelAfterUpdate),
139
+ maybeLatestModel: () => Option.some(modelAfterUpdate),
140
+ });
144
141
  return nextState.entries.length > maxEntries
145
142
  ? evictOldestSegment(nextState)
146
143
  : nextState;
@@ -161,16 +158,13 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
161
158
  return state;
162
159
  }
163
160
  return Array.match(state.entries, {
164
- onEmpty: () => ({
165
- ...state,
166
- initMountStarts: Array.appendAll(state.initMountStarts, mountStarts),
161
+ onEmpty: () => evo(state, {
162
+ initMountStarts: Array.appendAll(mountStarts),
167
163
  }),
168
- onNonEmpty: entries => ({
169
- ...state,
170
- entries: Array.modifyLastNonEmpty(entries, (last) => ({
171
- ...last,
172
- mountStarts: Array.appendAll(last.mountStarts, mountStarts),
173
- mountEnds: Array.appendAll(last.mountEnds, mountEnds),
164
+ onNonEmpty: entries => evo(state, {
165
+ entries: () => Array.modifyLastNonEmpty(entries, last => evo(last, {
166
+ mountStarts: Array.appendAll(mountStarts),
167
+ mountEnds: Array.appendAll(mountEnds),
174
168
  })),
175
169
  }),
176
170
  });
@@ -194,30 +188,37 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
194
188
  const jumpTo = (index) => Effect.gen(function* () {
195
189
  const state = yield* SubscriptionRef.get(stateRef);
196
190
  yield* bridge.render(resolveModel(state, index));
197
- yield* SubscriptionRef.set(stateRef, {
198
- ...state,
199
- isPaused: true,
200
- pausedAtIndex: index,
201
- });
191
+ yield* SubscriptionRef.set(stateRef, evo(state, {
192
+ isPaused: () => true,
193
+ pausedAtIndex: () => index,
194
+ }));
202
195
  });
203
196
  const resume = Effect.gen(function* () {
204
- yield* SubscriptionRef.update(stateRef, state => ({
205
- ...state,
206
- isPaused: false,
197
+ yield* SubscriptionRef.update(stateRef, state => evo(state, {
198
+ isPaused: () => false,
207
199
  }));
208
200
  yield* bridge.markRenderPending;
209
201
  });
210
- const clear = SubscriptionRef.update(stateRef, state => ({
211
- ...emptyState,
212
- maybeInitModel: state.maybeInitModel,
213
- initCommands: state.initCommands,
214
- initMountStarts: state.initMountStarts,
215
- keyframes: Option.match(state.maybeInitModel, {
216
- onNone: () => HashMap.empty(),
217
- onSome: model => HashMap.set(HashMap.empty(), 0, model),
218
- }),
219
- maybeLatestModel: state.maybeInitModel,
220
- }));
202
+ // NOTE: the paused snapshot is replayed off the entries array, so wiping
203
+ // entries while paused strands the runtime on a historical state with no
204
+ // path back to live. Refuse the write until resume.
205
+ const clear = SubscriptionRef.update(stateRef, state => {
206
+ if (state.isPaused) {
207
+ return state;
208
+ }
209
+ else {
210
+ return evo(state, {
211
+ entries: () => [],
212
+ startIndex: () => 0,
213
+ pausedAtIndex: () => 0,
214
+ keyframes: () => Option.match(state.maybeInitModel, {
215
+ onNone: () => HashMap.empty(),
216
+ onSome: model => HashMap.make([0, model]),
217
+ }),
218
+ maybeLatestModel: () => state.maybeInitModel,
219
+ });
220
+ }
221
+ });
221
222
  const getDiffAtIndex = (index) => Effect.gen(function* () {
222
223
  if (index === INIT_INDEX) {
223
224
  return emptyDiff;
@@ -20,6 +20,15 @@ type Hot = NonNullable<ImportMeta['hot']>;
20
20
  * without author-side changes. When `maybeMessageSchema` is `None`, dispatch
21
21
  * requests are rejected with an informative error.
22
22
  *
23
+ * The bridge also derives a JSON Schema document from `maybeMessageSchema`
24
+ * once at boot (via `Schema.toJsonSchemaDocument`) to fulfill
25
+ * `RequestGetMessageSchema`, so MCP clients can discover the exact Message
26
+ * shapes the runtime accepts without reading the application source. A few
27
+ * AST nodes (symbol-keyed structs, symbol-indexed records, tuples with
28
+ * post-rest elements) cause `Schema.toJsonSchemaDocument` to throw; the
29
+ * derivation is guarded so a failure logs a warning and the schema-discovery
30
+ * tool returns `None` rather than crashing the bridge.
31
+ *
23
32
  * Production-safe: callers must check `import.meta.hot` is defined before
24
33
  * invoking this. The function assumes a live HMR connection.
25
34
  */
@@ -1 +1 @@
1
- {"version":3,"file":"webSocketBridge.d.ts","sourceRoot":"","sources":["../../src/devTools/webSocketBridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,MAAM,EAIN,MAAM,EAEN,MAAM,IAAI,CAAC,EAGZ,MAAM,QAAQ,CAAA;AA+Bf,OAAO,EAAE,KAAK,aAAa,EAAc,MAAM,YAAY,CAAA;AAQ3D,KAAK,GAAG,GAAG,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAA;AAczC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,aAAa,EACpB,KAAK,GAAG,EACR,UAAU,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EACnD,oBAAoB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KACnD,MAAM,CAAC,MAAM,CAAC,IAAI,CAiFjB,CAAA"}
1
+ {"version":3,"file":"webSocketBridge.d.ts","sourceRoot":"","sources":["../../src/devTools/webSocketBridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,MAAM,EAIN,MAAM,EAEN,MAAM,IAAI,CAAC,EAGZ,MAAM,QAAQ,CAAA;AAwCf,OAAO,EAAE,KAAK,aAAa,EAAc,MAAM,YAAY,CAAA;AAQ3D,KAAK,GAAG,GAAG,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAA;AA4BzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,aAAa,EACpB,KAAK,GAAG,EACR,UAAU,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EACnD,oBAAoB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KACnD,MAAM,CAAC,MAAM,CAAC,IAAI,CAsFjB,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import { Array, Cause, Effect, Exit, HashMap, Match, Option, Order, Schema as S, SubscriptionRef, pipe, } from 'effect';
2
2
  import { OptionExt } from '../effectExtensions/index.js';
3
- import { EventConnected, EventDisconnected, EventFrame, KeyframeInfo, RequestFrame, ResponseDispatched, ResponseError, ResponseFrame, ResponseInit, ResponseKeyframes, ResponseMessage, ResponseMessages, ResponseModel, ResponseReplayed, ResponseResumed, ResponseRuntimeState, RuntimeInfo, } from './protocol.js';
3
+ import { EventConnected, EventDisconnected, EventFrame, KeyframeInfo, MessageSchemaDocumentResult, MessageSchemaIndexResult, RequestFrame, ResponseDispatched, ResponseError, ResponseFrame, ResponseInit, ResponseKeyframes, ResponseMessage, ResponseMessageSchema, ResponseMessages, ResponseModel, ResponseReplayed, ResponseResumed, ResponseRuntimeState, RuntimeInfo, } from './protocol.js';
4
+ import { diagnoseVariantPath, indexMessageSchemaDocument, narrowToVariant, splitVariantPath, } from './schemaSummarize.js';
4
5
  import { toInspectableValue, toSerializedCommand, toSerializedEntry, toSerializedMount, } from './serialize.js';
5
6
  import { INIT_INDEX } from './store.js';
6
7
  import { formatPathNotFound, resolvePath, summarizeValue, } from './summarize.js';
@@ -9,6 +10,15 @@ const RESPONSE_CHANNEL = 'foldkit:devTools:response';
9
10
  const EVENT_CHANNEL = 'foldkit:devTools:event';
10
11
  const generateConnectionId = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
11
12
  const currentAbsoluteIndex = (entriesLength, startIndex) => (entriesLength === 0 ? INIT_INDEX : startIndex + entriesLength - 1);
13
+ const tryDeriveJsonSchemaDocument = (schema) => {
14
+ try {
15
+ return Option.some(S.toJsonSchemaDocument(schema));
16
+ }
17
+ catch (error) {
18
+ console.warn('[foldkit:devTools] Failed to derive JSON Schema from Message Schema; foldkit_get_message_schema will return None.', error);
19
+ return Option.none();
20
+ }
21
+ };
12
22
  /**
13
23
  * Start the browser-side WebSocket bridge that exposes a Foldkit runtime's
14
24
  * DevToolsStore to an external MCP server (via the Vite plugin relay).
@@ -28,6 +38,15 @@ const currentAbsoluteIndex = (entriesLength, startIndex) => (entriesLength === 0
28
38
  * without author-side changes. When `maybeMessageSchema` is `None`, dispatch
29
39
  * requests are rejected with an informative error.
30
40
  *
41
+ * The bridge also derives a JSON Schema document from `maybeMessageSchema`
42
+ * once at boot (via `Schema.toJsonSchemaDocument`) to fulfill
43
+ * `RequestGetMessageSchema`, so MCP clients can discover the exact Message
44
+ * shapes the runtime accepts without reading the application source. A few
45
+ * AST nodes (symbol-keyed structs, symbol-indexed records, tuples with
46
+ * post-rest elements) cause `Schema.toJsonSchemaDocument` to throw; the
47
+ * derivation is guarded so a failure logs a warning and the schema-discovery
48
+ * tool returns `None` rather than crashing the bridge.
49
+ *
31
50
  * Production-safe: callers must check `import.meta.hot` is defined before
32
51
  * invoking this. The function assumes a live HMR connection.
33
52
  */
@@ -35,6 +54,7 @@ export const startWebSocketBridge = (store, hot, dispatch, maybeMessageSchema) =
35
54
  const connectionId = generateConnectionId();
36
55
  const capturedContext = yield* Effect.context();
37
56
  const maybeDispatchSchema = Option.map(maybeMessageSchema, S.toCodecJson);
57
+ const maybeJsonSchemaDocument = Option.flatMap(maybeMessageSchema, tryDeriveJsonSchemaDocument);
38
58
  const encodeEventFrame = S.encodeUnknownSync(EventFrame);
39
59
  const encodeResponseFrame = S.encodeUnknownSync(ResponseFrame);
40
60
  const sendEvent = (event) => {
@@ -54,7 +74,7 @@ export const startWebSocketBridge = (store, hot, dispatch, maybeMessageSchema) =
54
74
  }),
55
75
  }));
56
76
  const handleRequest = (id, request) => Effect.gen(function* () {
57
- const response = yield* dispatchRequest(store, dispatch, maybeDispatchSchema, request);
77
+ const response = yield* dispatchRequest(store, dispatch, maybeDispatchSchema, maybeJsonSchemaDocument, request);
58
78
  sendResponse(id, response);
59
79
  });
60
80
  const handleRequestFrame = (frame) => {
@@ -99,7 +119,47 @@ const readModelResponse = (store, index, maybePath, expand) => Effect.gen(functi
99
119
  }).pipe(Effect.catchCause(cause => Effect.succeed(ResponseError({
100
120
  reason: `Failed to read Model at index ${index}: ${Cause.pretty(cause)}`,
101
121
  }))));
102
- const dispatchRequest = (store, dispatch, maybeDispatchSchema, request) => Match.value(request).pipe(Match.tagsExhaustive({
122
+ const indexResponse = (document) => Option.match(indexMessageSchemaDocument(document), {
123
+ onNone: () => ResponseError({
124
+ reason: "Could not index Message Schema: the top-level shape is not a discriminated union of '_tag'-keyed structs. Open an issue if you see this against an Effect Schema released after foldkit's last sync.",
125
+ }),
126
+ onSome: variants => ResponseMessageSchema({
127
+ maybeResult: Option.some(MessageSchemaIndexResult({ index: { variants } })),
128
+ }),
129
+ });
130
+ const narrowResponse = (document, variantPath) => Option.match(narrowToVariant(document, variantPath), {
131
+ onNone: () => formatUnknownVariantError(document, variantPath),
132
+ onSome: narrowed => ResponseMessageSchema({
133
+ maybeResult: Option.some(MessageSchemaDocumentResult({ document: narrowed })),
134
+ }),
135
+ });
136
+ const buildMessageSchemaResponse = (maybeJsonSchemaDocument, maybeVariantTag) => Option.match(maybeJsonSchemaDocument, {
137
+ onNone: () => ResponseMessageSchema({ maybeResult: Option.none() }),
138
+ onSome: document => Option.match(maybeVariantTag, {
139
+ onNone: () => indexResponse(document),
140
+ onSome: variantTag => narrowResponse(document, variantTag),
141
+ }),
142
+ });
143
+ const formatUnknownVariantError = (document, variantPath) => {
144
+ const segments = splitVariantPath(variantPath);
145
+ return Option.match(diagnoseVariantPath(document, segments), {
146
+ onNone: () => ResponseError({
147
+ reason: `No Message variant at path '${variantPath}'. The runtime's Message Schema is not a discriminated union of '_tag'-keyed structs.`,
148
+ }),
149
+ onSome: ({ prefix, failingSegment, available }) => {
150
+ const prefixLabel = Array.isReadonlyArrayNonEmpty(prefix)
151
+ ? prefix.join('.')
152
+ : '<top level>';
153
+ const failingIsKnownTag = Option.exists(failingSegment, tag => available.includes(tag));
154
+ const failingTag = Option.getOrElse(failingSegment, () => '');
155
+ const reason = failingIsKnownTag
156
+ ? `No further structure to drill into at path '${variantPath}'. The variant '${failingTag}' at ${prefixLabel} does not carry exactly one tagged-union payload field, which is what the walker steps through. Idiomatic Foldkit Messages have at most one tagged-union field per variant (the 'message' field on Submodel wrappers, or a single value-type union); state surrounding a Submodel call belongs as an argument to the child's update/view, not as a sibling field on the parent Message.`
157
+ : `No Message variant at path '${variantPath}'. Available variants at ${prefixLabel}: ${available.join(', ')}.`;
158
+ return ResponseError({ reason });
159
+ },
160
+ });
161
+ };
162
+ const dispatchRequest = (store, dispatch, maybeDispatchSchema, maybeJsonSchemaDocument, request) => Match.value(request).pipe(Match.tagsExhaustive({
103
163
  RequestGetModel: ({ maybePath, expand }) => Effect.gen(function* () {
104
164
  const state = yield* SubscriptionRef.get(store.stateRef);
105
165
  const index = currentAbsoluteIndex(state.entries.length, state.startIndex);
@@ -166,6 +226,7 @@ const dispatchRequest = (store, dispatch, maybeDispatchSchema, request) => Match
166
226
  reason: `Invalid Message: ${error instanceof Error ? error.message : String(error)}\n\nReceived (typeof ${typeof message}): ${JSON.stringify(message)}`,
167
227
  })))),
168
228
  }),
229
+ RequestGetMessageSchema: ({ maybeVariantTag }) => Effect.succeed(buildMessageSchemaResponse(maybeJsonSchemaDocument, maybeVariantTag)),
169
230
  RequestListRuntimes: () => Effect.succeed(ResponseError({
170
231
  reason: 'RequestListRuntimes is plugin-handled and should not reach the runtime bridge',
171
232
  })),
@@ -1,4 +1,5 @@
1
1
  import { RoutingConfig } from './runtime.js';
2
2
  export declare const addNavigationEventListeners: <Message>(dispatch: (message: Message) => void, routingConfig: RoutingConfig<Message>) => void;
3
+ export declare const addLinkClickListener: <Message>(dispatch: (message: Message) => void, routingConfig: RoutingConfig<Message>) => void;
3
4
  export declare const addBfcacheRestoreListener: () => void;
4
5
  //# sourceMappingURL=browserListeners.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browserListeners.d.ts","sourceRoot":"","sources":["../../src/runtime/browserListeners.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,eAAO,MAAM,2BAA2B,GAAI,OAAO,EACjD,UAAU,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EACpC,eAAe,aAAa,CAAC,OAAO,CAAC,SAKtC,CAAA;AA6ED,eAAO,MAAM,yBAAyB,YASrC,CAAA"}
1
+ {"version":3,"file":"browserListeners.d.ts","sourceRoot":"","sources":["../../src/runtime/browserListeners.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,eAAO,MAAM,2BAA2B,GAAI,OAAO,EACjD,UAAU,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EACpC,eAAe,aAAa,CAAC,OAAO,CAAC,SAKtC,CAAA;AAaD,eAAO,MAAM,oBAAoB,GAAI,OAAO,EAC1C,UAAU,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EACpC,eAAe,aAAa,CAAC,OAAO,CAAC,SAoDtC,CAAA;AA4BD,eAAO,MAAM,yBAAyB,YASrC,CAAA"}
@@ -12,20 +12,32 @@ const addPopStateListener = (dispatch, routingConfig) => {
12
12
  };
13
13
  window.addEventListener('popstate', onPopState);
14
14
  };
15
- const addLinkClickListener = (dispatch, routingConfig) => {
15
+ export const addLinkClickListener = (dispatch, routingConfig) => {
16
16
  const onLinkClick = (event) => {
17
- const target = event.target;
18
- if (!(target instanceof Element)) {
17
+ const isNonPrimaryButton = event.button !== 0;
18
+ const isModifierKeyPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
19
+ const isDefaultPrevented = event.defaultPrevented;
20
+ if (isNonPrimaryButton || isModifierKeyPressed || isDefaultPrevented) {
19
21
  return;
20
22
  }
21
- const maybeLink = Option.fromNullishOr(target.closest('a'));
23
+ const eventTarget = event.target;
24
+ if (!(eventTarget instanceof Element)) {
25
+ return;
26
+ }
27
+ const maybeLink = Option.fromNullishOr(eventTarget.closest('a'));
22
28
  if (Option.isNone(maybeLink)) {
23
29
  return;
24
30
  }
25
- const { href } = maybeLink.value;
31
+ const link = maybeLink.value;
32
+ const { href } = link;
26
33
  if (String.isEmpty(href)) {
27
34
  return;
28
35
  }
36
+ const isNonSelfTarget = !String.isEmpty(link.target) && link.target !== '_self';
37
+ const isDownloadLink = link.hasAttribute('download');
38
+ if (isNonSelfTarget || isDownloadLink) {
39
+ return;
40
+ }
29
41
  event.preventDefault();
30
42
  const linkUrl = new URL(href);
31
43
  const currentUrl = new URL(window.location.href);
@@ -1 +1 @@
1
- {"version":3,"file":"bubbling.d.ts","sourceRoot":"","sources":["../../../src/test/apps/bubbling.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAKrD,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAOrC,QAAA,MAAM,OAAO,sLAAsD,CAAA;AACnE,KAAK,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIlC,eAAO,MAAM,YAAY,EAAE,KAG1B,CAAA;AAID,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,CAUrC,CAAA;AAMH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAajC,CAAA"}
1
+ {"version":3,"file":"bubbling.d.ts","sourceRoot":"","sources":["../../../src/test/apps/bubbling.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAKrD,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAOrC,QAAA,MAAM,OAAO,sLAAsD,CAAA;AACnE,KAAK,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIlC,eAAO,MAAM,YAAY,EAAE,KAG1B,CAAA;AAID,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,CAUrC,CAAA;AAIH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAgBnC,CAAA"}
@@ -24,8 +24,10 @@ export const update = (model, message) => M.value(message).pipe(M.withReturnType
24
24
  ],
25
25
  }));
26
26
  // VIEW
27
- const h = html();
28
- export const view = (model) => h.div([], [
29
- h.div([h.Role('option'), h.OnClick(ClickedContainer())], [h.span([], [`clicks=${model.clicks}`])]),
30
- h.div([h.Role('listitem'), h.OnDoubleClick(DoubleClickedContainer())], [h.span([], [`dbl=${model.doubleClicks}`])]),
31
- ]);
27
+ export const view = (model) => {
28
+ const h = html();
29
+ return h.div([], [
30
+ h.div([h.Role('option'), h.OnClick(ClickedContainer())], [h.span([], [`clicks=${model.clicks}`])]),
31
+ h.div([h.Role('listitem'), h.OnDoubleClick(DoubleClickedContainer())], [h.span([], [`dbl=${model.doubleClicks}`])]),
32
+ ]);
33
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../../../src/test/apps/counter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAExD,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAKrD,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,gBAAgB,8EAAwB,CAAA;AACrD,eAAO,MAAM,gBAAgB,8EAAwB,CAAA;AACrD,eAAO,MAAM,YAAY,0EAAoB,CAAA;AAC7C,eAAO,MAAM,gBAAgB;;EAA0C,CAAA;AACvE,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,qBAAqB,mFAA6B,CAAA;AAC/D,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,mBAAmB;;EAAgD,CAAA;AAChF,eAAO,MAAM,gBAAgB;;EAA6C,CAAA;AAE1E,eAAO,MAAM,OAAO;;;;;;IAUlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,eAAO,MAAM,UAAU;;;iBAIgC,CAAA;AAEvD,eAAO,MAAM,cAAc;;;;;iBAKyC,CAAA;AAIpE,eAAO,MAAM,YAAY,EAAE,KAA6B,CAAA;AAIxD,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAiCxD,CAAA;AAMH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAcjC,CAAA"}
1
+ {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../../../src/test/apps/counter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAExD,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAKrD,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,gBAAgB,8EAAwB,CAAA;AACrD,eAAO,MAAM,gBAAgB,8EAAwB,CAAA;AACrD,eAAO,MAAM,YAAY,0EAAoB,CAAA;AAC7C,eAAO,MAAM,gBAAgB;;EAA0C,CAAA;AACvE,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,qBAAqB,mFAA6B,CAAA;AAC/D,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,mBAAmB;;EAAgD,CAAA;AAChF,eAAO,MAAM,gBAAgB;;EAA6C,CAAA;AAE1E,eAAO,MAAM,OAAO;;;;;;IAUlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,eAAO,MAAM,UAAU;;;iBAIgC,CAAA;AAEvD,eAAO,MAAM,cAAc;;;;;iBAKyC,CAAA;AAIpE,eAAO,MAAM,YAAY,EAAE,KAA6B,CAAA;AAIxD,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAiCxD,CAAA;AAIH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAiBnC,CAAA"}