@typeonce/effect-machine-devtools 0.23.0 → 0.24.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 (50) hide show
  1. package/README.md +10 -4
  2. package/dist/DevServer.d.ts +2 -1
  3. package/dist/DevServer.d.ts.map +1 -1
  4. package/dist/DevServer.js.map +1 -1
  5. package/dist/DevToolsProtocol.d.ts +731 -17
  6. package/dist/DevToolsProtocol.d.ts.map +1 -1
  7. package/dist/DevToolsProtocol.js +191 -1
  8. package/dist/DevToolsProtocol.js.map +1 -1
  9. package/dist/MachineDocument.d.ts +78 -2
  10. package/dist/MachineDocument.d.ts.map +1 -1
  11. package/dist/MachineDocument.js +33 -1
  12. package/dist/MachineDocument.js.map +1 -1
  13. package/dist/MachineRegistry.d.ts +36 -6
  14. package/dist/MachineRegistry.d.ts.map +1 -1
  15. package/dist/ProjectInspector.d.ts +2 -0
  16. package/dist/ProjectInspector.d.ts.map +1 -1
  17. package/dist/ProjectInspector.js.map +1 -1
  18. package/dist/client/assets/index-B49Tjawc.js +14 -0
  19. package/dist/client/assets/index-BKH0cOG2.css +1 -0
  20. package/dist/client/index.html +2 -2
  21. package/dist/internal/devServer.d.ts +2 -1
  22. package/dist/internal/devServer.d.ts.map +1 -1
  23. package/dist/internal/devServer.js +68 -7
  24. package/dist/internal/devServer.js.map +1 -1
  25. package/dist/internal/evaluationWorker.d.ts.map +1 -1
  26. package/dist/internal/evaluationWorker.js +279 -4
  27. package/dist/internal/evaluationWorker.js.map +1 -1
  28. package/dist/internal/machineDocument.d.ts.map +1 -1
  29. package/dist/internal/machineDocument.js +63 -2
  30. package/dist/internal/machineDocument.js.map +1 -1
  31. package/dist/internal/projectInspector.d.ts.map +1 -1
  32. package/dist/internal/projectInspector.js +26 -10
  33. package/dist/internal/projectInspector.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/DevServer.ts +6 -2
  36. package/src/DevToolsProtocol.ts +286 -1
  37. package/src/MachineDocument.ts +54 -1
  38. package/src/ProjectInspector.ts +5 -0
  39. package/src/internal/browser/input-form.ts +669 -0
  40. package/src/internal/browser/planner-example.ts +143 -0
  41. package/src/internal/browser/simulation-client.ts +20 -0
  42. package/src/internal/browser/styles.css +323 -3
  43. package/src/internal/browser/visualizer-app.ts +395 -34
  44. package/src/internal/browser/visualizer.ts +1 -1
  45. package/src/internal/devServer.ts +91 -8
  46. package/src/internal/evaluationWorker.ts +390 -8
  47. package/src/internal/machineDocument.ts +75 -2
  48. package/src/internal/projectInspector.ts +58 -15
  49. package/dist/client/assets/index-BGOhE3Ng.css +0 -1
  50. package/dist/client/assets/index-DiqGhsGR.js +0 -14
@@ -1,11 +1,18 @@
1
- import type { Diagnostic } from "../../DevToolsProtocol.js"
1
+ import {
2
+ type Diagnostic,
3
+ protocolVersion,
4
+ type SimulationFrame,
5
+ type SimulationReady,
6
+ type SimulationRequest
7
+ } from "../../DevToolsProtocol.js"
2
8
  import type {
3
9
  Activity as VisualizationActivity,
4
10
  Branch as VisualizationBranch,
5
11
  MachineDocument as VisualizationDocument,
6
12
  Transition as VisualizationTransition
7
13
  } from "../../MachineDocument.js"
8
- import * as MachineSimulator from "../../MachineSimulator.js"
14
+ import { type InputForm, renderInputForm } from "./input-form.js"
15
+ import { requestSimulation } from "./simulation-client.js"
9
16
  import {
10
17
  type EventInspection,
11
18
  type IncomingTransition,
@@ -159,22 +166,27 @@ const inspectionSection = (title: string, count: number): HTMLElement => {
159
166
  return header
160
167
  }
161
168
 
162
- const simulationResultMessage = (result: MachineSimulator.StepResult): string => {
163
- if (result._tag === "Applied") return `${result.event} applied · runtime code was skipped`
164
- if (result._tag === "Blocked") return `${result.event} is not enabled in the current topology`
165
- const reasons: Record<MachineSimulator.Indeterminate["reason"], string> = {
166
- "multiple-transitions": "multiple active transitions",
167
- "declinable-transition": "acceptance depends on runtime code",
168
- "conditional-branches": "the selected branch depends on runtime code",
169
- "history-target": "history resolution needs runtime state",
170
- "choice-target": "choice resolution needs runtime code",
171
- "missing-target": "the target is not present in the document"
172
- }
173
- return `${result.event} was not applied · ${reasons[result.reason]}`
169
+ const formSection = (title: string): HTMLElement => {
170
+ const header = createElement("div", "section-heading")
171
+ header.append(createElement("h3", undefined, title))
172
+ return header
173
+ }
174
+
175
+ const prettyJson = (value: unknown): string => JSON.stringify(value, null, 2)
176
+
177
+ const jsonBlock = (value: unknown): HTMLElement => createElement("pre", "json-value", prettyJson(value))
178
+
179
+ const eventName = (value: unknown): string =>
180
+ typeof value === "object" && value !== null && "_tag" in value ? String(value._tag) : "event"
181
+
182
+ const activeTopology = (paths: ReadonlyArray<string>): string => {
183
+ const leaves = paths.filter((path) => !paths.some((candidate) => candidate.startsWith(`${path}.`)))
184
+ return leaves.map((path) => path.split(".").at(-1) ?? path).join(" + ") || "none"
174
185
  }
175
186
 
176
187
  export const renderVisualizer = (
177
188
  root: HTMLElement,
189
+ machineKey: string,
178
190
  visualization: VisualizationDocument,
179
191
  diagnostics: ReadonlyArray<Diagnostic> = []
180
192
  ): void => {
@@ -183,13 +195,17 @@ export const renderVisualizer = (
183
195
  const nodes = new Map<string, HTMLElement>()
184
196
  const statuses = new Map<string, HTMLElement>()
185
197
  const eventButtons = new Map<string, HTMLElement>()
198
+ const eventSchemas = new Map(visualization.inputs.events.map(({ event, schema }) => [event, schema]))
186
199
  const relatedPaths = new Set<string>()
187
200
  let selectedPath: string | undefined
188
201
  let selectedEvent: string | undefined
189
- let simulation: MachineSimulator.Session | undefined
202
+ let selectedFrame: SimulationFrame | undefined
203
+ let simulation: SimulationReady | undefined
204
+ let simulationPending = false
205
+ let activeInputForm: InputForm | undefined
190
206
 
191
- const activePaths = (): ReadonlyArray<string> => simulation?.snapshot.activePaths ?? model.activePaths
192
- const candidateEvents = (): ReadonlyArray<string> => simulation?.snapshot.candidateEvents ?? model.candidateEvents
207
+ const activePaths = (): ReadonlyArray<string> => simulation?.current.activePaths ?? model.activePaths
208
+ const candidateEvents = (): ReadonlyArray<string> => simulation?.current.candidateEvents ?? model.candidateEvents
193
209
 
194
210
  const shell = createElement("main", "app-shell")
195
211
  const workspace = createElement("section", "workspace")
@@ -213,6 +229,7 @@ export const renderVisualizer = (
213
229
  simulationButton.disabled = model.roots.length === 0
214
230
 
215
231
  const renderEmptyInspector = (): void => {
232
+ activeInputForm = undefined
216
233
  inspector.replaceChildren()
217
234
  const summary = createElement("div", "inspector-empty")
218
235
  summary.append(createElement("span", "inspector-empty-kind", "Machine"))
@@ -229,6 +246,7 @@ export const renderVisualizer = (
229
246
  }
230
247
 
231
248
  const renderInspection = (inspection: StateInspection): void => {
249
+ activeInputForm = undefined
232
250
  inspector.replaceChildren()
233
251
  const header = createElement("header", "inspector-header")
234
252
  const breadcrumbs = createElement("nav", "breadcrumbs")
@@ -290,6 +308,7 @@ export const renderVisualizer = (
290
308
  }
291
309
 
292
310
  const renderEventInspection = (inspection: EventInspection): void => {
311
+ activeInputForm = undefined
293
312
  inspector.replaceChildren()
294
313
  const header = createElement("header", "inspector-header")
295
314
  const eyebrow = createElement("div", "inspector-eyebrow")
@@ -303,6 +322,55 @@ export const renderVisualizer = (
303
322
  ]))
304
323
  inspector.append(header)
305
324
 
325
+ if (simulation !== undefined && candidate) {
326
+ const schema = eventSchemas.get(inspection.event)
327
+ const composer = createElement("section", "inspector-section simulation-composer")
328
+ composer.append(formSection("Event input"))
329
+ if (schema === undefined) {
330
+ composer.append(createElement(
331
+ "p",
332
+ "input-unsupported",
333
+ "No public input schema is available for this event."
334
+ ))
335
+ } else {
336
+ const input = renderInputForm(schema, {
337
+ name: inspection.event,
338
+ fixed: { _tag: inspection.event },
339
+ omit: ["_tag"]
340
+ })
341
+ activeInputForm = input
342
+ const send = createElement("button", "simulation-action", `Send ${inspection.event}`)
343
+ send.type = "submit"
344
+ send.disabled = simulationPending || !input.supported
345
+ input.element.addEventListener("submit", (event) => {
346
+ event.preventDefault()
347
+ const result = input.read()
348
+ const source = visualization.source
349
+ if (!result.ok || source === null || simulation === undefined) return
350
+ void runSimulation({
351
+ _tag: "SendSimulationEvent",
352
+ protocolVersion,
353
+ key: machineKey,
354
+ revision: visualization.revision,
355
+ source,
356
+ step: simulation.step,
357
+ snapshot: simulation.snapshot,
358
+ event: result.value as never
359
+ })
360
+ })
361
+ input.element.append(send)
362
+ composer.append(
363
+ input.element,
364
+ createElement(
365
+ "p",
366
+ "simulation-note",
367
+ "The real planner validates this event and shows every synchronous transition it selects."
368
+ )
369
+ )
370
+ }
371
+ inspector.append(composer)
372
+ }
373
+
306
374
  const transitions = createElement("section", "inspector-section")
307
375
  transitions.append(inspectionSection("Transitions", inspection.transitions.length))
308
376
  inspection.transitions.forEach((transition) =>
@@ -311,6 +379,265 @@ export const renderVisualizer = (
311
379
  inspector.append(transitions)
312
380
  }
313
381
 
382
+ const renderSimulationFailure = (failureDiagnostics: ReadonlyArray<Diagnostic>): void => {
383
+ activeInputForm = undefined
384
+ inspector.replaceChildren()
385
+ const header = createElement("header", "inspector-header")
386
+ const eyebrow = createElement("div", "inspector-eyebrow")
387
+ eyebrow.append(badge("planner error", "error"))
388
+ header.append(eyebrow, createElement("h2", undefined, "Simulation could not continue"))
389
+ inspector.append(header)
390
+ const list = createElement("section", "inspector-section trace-list")
391
+ failureDiagnostics.forEach((item) => {
392
+ const card = createElement("article", "inspection-card trace-card")
393
+ card.append(createElement("strong", undefined, item.code), createElement("pre", "trace-error", item.message))
394
+ list.append(card)
395
+ })
396
+ inspector.append(list)
397
+ }
398
+
399
+ const renderPathGroup = (label: string, paths: ReadonlyArray<string>): HTMLElement => {
400
+ const group = createElement("div", "trace-path-group")
401
+ group.append(createElement("span", "trace-label", label))
402
+ const values = createElement("div", "trace-paths")
403
+ if (paths.length === 0) values.append(createElement("span", "section-empty", "none"))
404
+ paths.forEach((path) => values.append(stateLink(path, path, navigateToState)))
405
+ group.append(values)
406
+ return group
407
+ }
408
+
409
+ const renderTraceValues = (label: string, values: ReadonlyArray<unknown>): HTMLElement => {
410
+ const section = createElement("div", "trace-values")
411
+ section.append(createElement("span", "trace-label", label))
412
+ if (values.length === 0) {
413
+ section.append(createElement("span", "section-empty", "none"))
414
+ } else {
415
+ values.forEach((value) => section.append(jsonBlock(value)))
416
+ }
417
+ return section
418
+ }
419
+
420
+ const renderSimulationTrace = (frame: SimulationFrame): void => {
421
+ activeInputForm = undefined
422
+ inspector.replaceChildren()
423
+ const header = createElement("header", "inspector-header trace-header")
424
+ const eyebrow = createElement("div", "inspector-eyebrow")
425
+ eyebrow.append(badge("planned", "active"))
426
+ if (frame.done) eyebrow.append(badge("done", "state"))
427
+ const title = frame.trigger._tag === "Initial"
428
+ ? "Initial plan"
429
+ : `Event · ${eventName(frame.trigger.event)}`
430
+ header.append(eyebrow, createElement("h2", undefined, title))
431
+ header.append(metadata([
432
+ ["Step", String(frame.step)],
433
+ ["Microsteps", String(frame.microsteps.length)],
434
+ ["Active before", String(frame.before.activePaths.length)],
435
+ ["Active after", String(frame.after.activePaths.length)]
436
+ ]))
437
+ const received = frame.trigger._tag === "Initial" ? frame.trigger.input : frame.trigger.event
438
+ if (received !== undefined) header.append(jsonBlock(received))
439
+ inspector.append(header)
440
+
441
+ const topology = createElement("section", "inspector-section trace-topology")
442
+ topology.append(inspectionSection("Topology change", frame.microsteps.length))
443
+ topology.append(
444
+ renderPathGroup("Before", frame.before.activePaths),
445
+ renderPathGroup("After", frame.after.activePaths)
446
+ )
447
+ inspector.append(topology)
448
+
449
+ const steps = createElement("section", "inspector-section trace-list")
450
+ steps.append(inspectionSection("Microsteps", frame.microsteps.length))
451
+ if (frame.microsteps.length === 0) {
452
+ steps.append(createElement(
453
+ "p",
454
+ "section-empty",
455
+ frame.trigger._tag === "Initial"
456
+ ? "No automatic microsteps were needed."
457
+ : "No transition accepted this event."
458
+ ))
459
+ }
460
+ frame.microsteps.forEach((step) => {
461
+ const card = createElement("article", "inspection-card trace-card")
462
+ const cardHeader = createElement("div", "card-header")
463
+ const cardTitle = createElement("div", "card-title")
464
+ cardTitle.append(
465
+ badge(`#${step.index + 1}`, "count"),
466
+ createElement("strong", undefined, eventName(step.event))
467
+ )
468
+ cardHeader.append(cardTitle, badge(step.changed ? "changed" : "unchanged", step.changed ? "active" : "neutral"))
469
+ card.append(cardHeader)
470
+ if (step.transitions.length > 0) {
471
+ const selected = createElement("div", "trace-transitions")
472
+ selected.append(createElement("span", "trace-label", "Selected transitions"))
473
+ step.transitions.forEach((transition) => {
474
+ const row = createElement("div", "trace-transition")
475
+ row.append(stateLink(transition.source, transition.source, navigateToState))
476
+ row.append(createElement("span", "branch-arrow", "→"))
477
+ const target = transition.resolvedTarget ?? transition.target
478
+ if (target === null) row.append(createElement("span", "branch-target", "No target"))
479
+ else row.append(stateLink(target, target, navigateToState))
480
+ if (transition.branchKey !== null) row.append(badge(transition.branchKey, "condition"))
481
+ if (transition.reenter) row.append(badge("reenter"))
482
+ selected.append(row)
483
+ if (transition.updates.length > 0) selected.append(renderPathGroup("Updates", transition.updates))
484
+ })
485
+ card.append(selected)
486
+ }
487
+ card.append(renderPathGroup("Exit", step.exitPaths), renderPathGroup("Entry", step.entryPaths))
488
+ if (step.raisedEvents.length > 0) card.append(renderTraceValues("Raised", step.raisedEvents))
489
+ if (step.emittedEvents.length > 0) card.append(renderTraceValues("Emitted", step.emittedEvents))
490
+ if (step.commands.length > 0) card.append(renderTraceValues("Commands", step.commands))
491
+ card.append(renderPathGroup("Active after", step.activePaths))
492
+ steps.append(card)
493
+ })
494
+ inspector.append(steps)
495
+
496
+ if (frame.commands.length > 0 || frame.emittedEvents.length > 0 || frame.output !== undefined) {
497
+ const results = createElement("section", "inspector-section trace-results")
498
+ results.append(inspectionSection("Plan result", frame.commands.length + frame.emittedEvents.length))
499
+ if (frame.commands.length > 0) results.append(renderTraceValues("Planned commands", frame.commands))
500
+ if (frame.emittedEvents.length > 0) results.append(renderTraceValues("Emitted events", frame.emittedEvents))
501
+ if (frame.output !== undefined) results.append(renderTraceValues("Output", [frame.output]))
502
+ inspector.append(results)
503
+ }
504
+ }
505
+
506
+ const renderStartSimulation = (): void => {
507
+ activeInputForm = undefined
508
+ inspector.replaceChildren()
509
+ const header = createElement("header", "inspector-header")
510
+ const eyebrow = createElement("div", "inspector-eyebrow")
511
+ eyebrow.append(badge("planner", "active"))
512
+ header.append(eyebrow, createElement("h2", undefined, "Start simulation"))
513
+ inspector.append(header)
514
+ const composer = createElement("section", "inspector-section simulation-composer")
515
+ composer.append(formSection("Machine input"))
516
+ const input = visualization.inputs.machine
517
+ if (input === null) {
518
+ composer.append(createElement("p", "section-empty", "This machine does not declare startup input."))
519
+ inspector.append(composer)
520
+ return
521
+ }
522
+ const form = renderInputForm(input, { name: "Machine input" })
523
+ activeInputForm = form
524
+ const start = createElement("button", "simulation-action", "Start simulation")
525
+ start.type = "submit"
526
+ start.disabled = simulationPending || visualization.source === null || !form.supported
527
+ form.element.addEventListener("submit", (event) => {
528
+ event.preventDefault()
529
+ const source = visualization.source
530
+ if (source === null) return
531
+ const result = form.read()
532
+ if (!result.ok) return
533
+ const request: SimulationRequest = {
534
+ _tag: "StartSimulation",
535
+ protocolVersion,
536
+ key: machineKey,
537
+ revision: visualization.revision,
538
+ source,
539
+ input: result.value as never
540
+ }
541
+ void runSimulation(request)
542
+ })
543
+ form.element.append(start)
544
+ composer.append(
545
+ form.element,
546
+ createElement(
547
+ "p",
548
+ "simulation-note",
549
+ "Initialization and synchronous callbacks run in isolation. Runtime activities and planned commands are not started."
550
+ )
551
+ )
552
+ inspector.append(composer)
553
+ }
554
+
555
+ async function runSimulation(request: SimulationRequest): Promise<void> {
556
+ if (simulationPending) return
557
+ simulationPending = true
558
+ activeInputForm?.clearIssues()
559
+ activeInputForm?.setPending(true)
560
+ simulationFeedback.textContent = "Planning in an isolated worker…"
561
+ simulationFeedback.dataset.status = "pending"
562
+ updateSimulationUi()
563
+ try {
564
+ const result = await requestSimulation(request)
565
+ if (result._tag === "SimulationFailed") {
566
+ if (result.inputIssues.length > 0 && activeInputForm !== undefined) {
567
+ simulationFeedback.textContent = "Some input fields are invalid"
568
+ simulationFeedback.dataset.status = "error"
569
+ activeInputForm.setIssues(result.inputIssues)
570
+ return
571
+ }
572
+ selectedFrame = undefined
573
+ if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
574
+ selectedEvent = undefined
575
+ simulationFeedback.textContent = result.diagnostics[0]?.message ?? "Simulation failed"
576
+ simulationFeedback.dataset.status = "error"
577
+ renderSimulationFailure(result.diagnostics)
578
+ return
579
+ }
580
+ simulation = result
581
+ activeInputForm = undefined
582
+ if (selectedPath !== undefined) {
583
+ nodes.get(selectedPath)?.classList.remove("is-selected")
584
+ rows.get(selectedPath)?.setAttribute("aria-selected", "false")
585
+ }
586
+ if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
587
+ selectedPath = undefined
588
+ selectedEvent = undefined
589
+ selectedFrame = result.frame
590
+ clearRelations()
591
+ result.frame.microsteps.forEach((step) => {
592
+ step.transitions.forEach((transition) => {
593
+ relatedPaths.add(transition.source)
594
+ nodes.get(transition.source)?.classList.add("is-related-source")
595
+ })
596
+ step.entryPaths.forEach((path) => {
597
+ relatedPaths.add(path)
598
+ nodes.get(path)?.classList.add("is-related-target")
599
+ })
600
+ step.transitions.flatMap((transition) => transition.updates).forEach((path) => {
601
+ relatedPaths.add(path)
602
+ nodes.get(path)?.classList.add("is-related-update")
603
+ })
604
+ })
605
+ clearButton.disabled = false
606
+ const before = activeTopology(result.frame.before.activePaths)
607
+ const after = activeTopology(result.frame.after.activePaths)
608
+ simulationFeedback.textContent = result.frame.trigger._tag === "Initial"
609
+ ? `Started in ${after}`
610
+ : result.frame.microsteps.length === 0
611
+ ? `${eventName(result.frame.trigger.event)} was not accepted · remained in ${after}`
612
+ : before === after
613
+ ? `${eventName(result.frame.trigger.event)} handled · remained in ${after}`
614
+ : `${before} → ${after} · ${result.frame.microsteps.length} microstep${
615
+ result.frame.microsteps.length === 1 ? "" : "s"
616
+ }`
617
+ simulationFeedback.dataset.status = "applied"
618
+ } catch (cause) {
619
+ selectedFrame = undefined
620
+ if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
621
+ selectedEvent = undefined
622
+ const failure: Diagnostic = {
623
+ severity: "error",
624
+ code: "simulation-request-failed",
625
+ message: cause instanceof Error ? cause.message : String(cause),
626
+ location: visualization.source === null
627
+ ? null
628
+ : { file: visualization.source.file, line: null, column: null },
629
+ statePath: null
630
+ }
631
+ simulationFeedback.textContent = failure.message
632
+ simulationFeedback.dataset.status = "error"
633
+ renderSimulationFailure([failure])
634
+ } finally {
635
+ simulationPending = false
636
+ activeInputForm?.setPending(false)
637
+ updateSimulationUi()
638
+ }
639
+ }
640
+
314
641
  const clearRelations = (): void => {
315
642
  for (const path of relatedPaths) {
316
643
  nodes.get(path)?.classList.remove("is-related-source", "is-related-target", "is-related-update")
@@ -327,6 +654,7 @@ export const renderVisualizer = (
327
654
  clearRelations()
328
655
  selectedPath = undefined
329
656
  selectedEvent = undefined
657
+ selectedFrame = undefined
330
658
  clearButton.disabled = true
331
659
  renderEmptyInspector()
332
660
  }
@@ -380,6 +708,7 @@ export const renderVisualizer = (
380
708
  if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
381
709
  selectedPath = path
382
710
  selectedEvent = undefined
711
+ selectedFrame = undefined
383
712
  nodes.get(path)?.classList.add("is-selected")
384
713
  rows.get(path)?.setAttribute("aria-selected", "true")
385
714
  markRelatedStates(inspection)
@@ -404,10 +733,32 @@ export const renderVisualizer = (
404
733
  if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
405
734
  selectedPath = undefined
406
735
  selectedEvent = event
736
+ selectedFrame = undefined
407
737
  eventButtons.get(event)?.classList.add("is-selected")
408
738
  clearRelations()
409
739
  markTransitions(inspection.transitions)
410
740
  clearButton.disabled = false
741
+ const schema = eventSchemas.get(event)
742
+ if (simulation !== undefined && candidateEvents().includes(event) && schema !== undefined) {
743
+ const input = renderInputForm(schema, { name: event, fixed: { _tag: event }, omit: ["_tag"] })
744
+ if (!input.hasFields && input.supported) {
745
+ const result = input.read()
746
+ const source = visualization.source
747
+ if (result.ok && source !== null) {
748
+ void runSimulation({
749
+ _tag: "SendSimulationEvent",
750
+ protocolVersion,
751
+ key: machineKey,
752
+ revision: visualization.revision,
753
+ source,
754
+ step: simulation.step,
755
+ snapshot: simulation.snapshot,
756
+ event: result.value as never
757
+ })
758
+ return
759
+ }
760
+ }
761
+ }
411
762
  renderEventInspection(inspection)
412
763
  }
413
764
 
@@ -511,16 +862,8 @@ export const renderVisualizer = (
511
862
  candidates.forEach((event) => {
512
863
  const button = createElement("button", `event-button${event === selectedEvent ? " is-selected" : ""}`, event)
513
864
  button.type = "button"
514
- button.addEventListener("click", () => {
515
- if (simulation !== undefined) {
516
- const result = MachineSimulator.send(simulation, event)
517
- if (result._tag === "Applied") simulation = { document: visualization, snapshot: result.session }
518
- simulationFeedback.textContent = simulationResultMessage(result)
519
- simulationFeedback.dataset.status = result._tag.toLowerCase()
520
- updateSimulationUi()
521
- }
522
- selectEvent(event)
523
- })
865
+ button.disabled = simulationPending
866
+ button.addEventListener("click", () => selectEvent(event))
524
867
  eventButtons.set(event, button)
525
868
  events.append(button)
526
869
  })
@@ -536,34 +879,52 @@ export const renderVisualizer = (
536
879
  const hasRuntimeState = simulation !== undefined || model.hasSnapshot
537
880
  runtimeDot.classList.toggle("has-snapshot", hasRuntimeState)
538
881
  runtimeText.textContent = simulation !== undefined
539
- ? `${active.size} active · step ${simulation.snapshot.step}`
882
+ ? `${active.size} active · step ${simulation.step}`
540
883
  : diagnostics.length > 0
541
884
  ? "Partial"
542
885
  : model.hasSnapshot
543
886
  ? `${active.size} active`
544
887
  : "Structure only"
545
888
  simulationButton.textContent = simulation === undefined ? "Start simulation" : "Reset simulation"
889
+ simulationButton.disabled = simulationPending || model.roots.length === 0 || visualization.source === null
546
890
  revealActiveButton.disabled = active.size === 0
547
891
  events.hidden = !hasRuntimeState
548
- simulationFeedback.hidden = simulation === undefined
892
+ simulationFeedback.hidden = simulation === undefined && !simulationPending && simulationFeedback.textContent === ""
549
893
  renderEventButtons()
550
- if (selectedPath !== undefined) {
894
+ if (selectedFrame !== undefined) {
895
+ renderSimulationTrace(selectedFrame)
896
+ } else if (selectedPath !== undefined) {
551
897
  const inspection = model.inspectState(selectedPath)
552
898
  if (inspection !== undefined) renderInspection(inspection)
553
- } else if (selectedEvent !== undefined) {
899
+ } else if (selectedEvent !== undefined && activeInputForm === undefined) {
554
900
  renderEventInspection(model.inspectEvent(selectedEvent))
555
901
  }
556
902
  }
557
903
 
558
904
  simulationButton.addEventListener("click", () => {
559
905
  if (simulation === undefined) {
560
- simulation = MachineSimulator.start(visualization)
561
- simulationFeedback.textContent = "Best-effort simulation started · user code will not run"
562
- simulationFeedback.dataset.status = "applied"
906
+ selectedPath = undefined
907
+ selectedEvent = undefined
908
+ selectedFrame = undefined
909
+ clearRelations()
910
+ clearButton.disabled = false
911
+ if (visualization.inputs.machine === null && visualization.source !== null) {
912
+ void runSimulation({
913
+ _tag: "StartSimulation",
914
+ protocolVersion,
915
+ key: machineKey,
916
+ revision: visualization.revision,
917
+ source: visualization.source
918
+ })
919
+ } else {
920
+ renderStartSimulation()
921
+ }
563
922
  } else {
564
923
  simulation = undefined
924
+ selectedFrame = undefined
565
925
  simulationFeedback.textContent = ""
566
926
  delete simulationFeedback.dataset.status
927
+ clearSelection()
567
928
  }
568
929
  updateSimulationUi()
569
930
  })
@@ -22,7 +22,7 @@ export const mountVisualizer = (root: HTMLElement, source: MachineResult): void
22
22
  switch (source._tag) {
23
23
  case "Ready":
24
24
  case "Partial":
25
- renderVisualizer(root, source.document, source.diagnostics)
25
+ renderVisualizer(root, source.key, source.document, source.diagnostics)
26
26
  break
27
27
  case "Failed":
28
28
  renderError(root, source)