mockpit 1.2.0 → 1.3.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 (51) hide show
  1. package/README.md +13 -8
  2. package/bin/mockpit.js +116 -16
  3. package/dist/mcp/server.js +10 -7
  4. package/dist/server/apiViews.d.ts +118 -17
  5. package/dist/server/apiViews.js +66 -8
  6. package/dist/server/app.d.ts +2 -1
  7. package/dist/server/app.js +917 -144
  8. package/dist/server/decisions.d.ts +23 -0
  9. package/dist/server/decisions.js +99 -0
  10. package/dist/server/designGuide.js +45 -10
  11. package/dist/server/embeds.d.ts +25 -0
  12. package/dist/server/embeds.js +118 -0
  13. package/dist/server/eventLog.js +2 -1
  14. package/dist/server/feedbackBatch.d.ts +12 -1
  15. package/dist/server/feedbackBatch.js +20 -3
  16. package/dist/server/fills.d.ts +41 -0
  17. package/dist/server/fills.js +328 -0
  18. package/dist/server/index.js +15 -1
  19. package/dist/server/knobs.d.ts +6 -2
  20. package/dist/server/knobs.js +11 -1
  21. package/dist/server/mcpHttp.d.ts +3 -0
  22. package/dist/server/mcpHttp.js +3 -0
  23. package/dist/server/mcpSpec.d.ts +13 -13
  24. package/dist/server/mcpSpec.js +73 -8
  25. package/dist/server/postSurfaces.js +3 -12
  26. package/dist/server/richRender.js +3 -1
  27. package/dist/server/run.js +4 -0
  28. package/dist/server/runApi.d.ts +1 -1
  29. package/dist/server/runApi.js +4 -3
  30. package/dist/server/sqlStore.d.ts +89 -4
  31. package/dist/server/sqlStore.js +149 -23
  32. package/dist/server/surfacePage.d.ts +6 -2
  33. package/dist/server/surfacePage.js +142 -10
  34. package/dist/server/themes.d.ts +2 -0
  35. package/dist/server/themes.js +23 -3
  36. package/dist/server/types.d.ts +73 -11
  37. package/dist/server/types.js +42 -2
  38. package/dist/server/viewerRequest.d.ts +2 -0
  39. package/dist/server/viewerRequest.js +14 -0
  40. package/guide/topics/asks.md +50 -3
  41. package/guide/topics/feedback.md +89 -11
  42. package/guide/topics/html.md +5 -1
  43. package/guide/topics/http.md +2 -1
  44. package/guide/topics/knobs.md +2 -1
  45. package/guide/topics/surfaces.md +24 -8
  46. package/package.json +4 -13
  47. package/skills/launch-video/SKILL.md +1 -1
  48. package/skills/mockpit/SKILL.md +12 -4
  49. package/viewer/dist/index.html +8 -8
  50. package/dist/server/slots.d.ts +0 -14
  51. package/dist/server/slots.js +0 -71
@@ -1,3 +1,4 @@
1
+ import { scanFills, slotCss } from "./fills.js";
1
2
  import { CDN_ALLOWLIST, checkCdnUrl } from "./cdn.js";
2
3
  import { CORE_CSS, KITS, kitAssets } from "./kits.js";
3
4
  import { expandIcons } from "./icons.js";
@@ -63,7 +64,11 @@ const TOKENS_CSS = `
63
64
  --border-radius-lg: 12px;
64
65
  --border-radius-xl: 16px;
65
66
  }
66
- html { box-sizing: border-box; scrollbar-width: none; }
67
+ /* A page that scrolls inside a host's frame stops at its own ends instead of
68
+ handing the rest of a swipe to the host (Safari reads the root's value), and
69
+ a pinch on it can't zoom the whole viewer around it. */
70
+ html { box-sizing: border-box; scrollbar-width: none; overscroll-behavior: contain; touch-action: pan-x pan-y; }
71
+ body { overscroll-behavior: contain; }
67
72
  html::-webkit-scrollbar, body::-webkit-scrollbar { display: none; }
68
73
  *, *::before, *::after { box-sizing: inherit; }
69
74
  body {
@@ -356,7 +361,7 @@ export const HIT_TEST_JS = `
356
361
  // copy {text} window.copyToClipboard (BRIDGE_JS)
357
362
  // switch-session {key} Cmd+Opt+ArrowUp/Down (BRIDGE_JS)
358
363
  // hit-test-result {ref, path, text, rect} reply to hit-test (HIT_TEST_JS)
359
- // parts {version, parts, height, scroll:{x,y}, viewport:{w,h}} (PARTS_JS)
364
+ // parts {version, parts, height, scroll:{x,y}, viewport:{w,h,scrollW}} (PARTS_JS)
360
365
  // parts[i] = {name, label, key, box:{x,y,w,h}, visible, fixed, depth, parent, order}
361
366
  // box is in DOCUMENT px (viewport rect + scroll), so the host draws
362
367
  // (box − scroll) × scale, which is also right for fixed and sticky parts.
@@ -370,6 +375,23 @@ export const HIT_TEST_JS = `
370
375
  // 50/150/400/1000ms timers, and on any layout-affecting change; never
371
376
  // twice in a row with identical content.
372
377
  // hit {ref, part, key} reply to hit; part = name|null
378
+ // goto {version, to} a click on [data-goto] while the host
379
+ // has Try on (PARTS_JS); to is the
380
+ // attribute as written ("next", "prev",
381
+ // "<mock>:<state>"), ≤200 chars. The host
382
+ // honours it only while Try is on, from
383
+ // the frame on stage at its version, and
384
+ // only to a screen of this project:
385
+ // never a URL
386
+ // tap {x, y} a finger's tap in document px (PARTS_JS);
387
+ // a drag never reports
388
+ // ready {version} the knob runtime is listening (KNOBS_JS,
389
+ // from <head>); values can be sent now
390
+ // pinch {phase, a:{x,y}, b:{x,y}} two fingers on the page, in viewport px
391
+ // zoom {x, y, dy} ⌘/Ctrl+scroll or a trackpad pinch, in viewport px
392
+ // (PARTS_JS); phase start|move|end. The
393
+ // page's own two-finger touches are
394
+ // cancelled: the host zooms its canvas
373
395
  //
374
396
  // host → frame
375
397
  // hit-test {ref, x, y} x,y normalized 0..1 of the document box (Mark tool)
@@ -383,6 +405,9 @@ export const HIT_TEST_JS = `
383
405
  // overlay, forwarded)
384
406
  // knobs {values} {path: value}, merged into the current values
385
407
  // and applied exactly like the `?k=` preamble
408
+ // try {on} Try is on or off: while on, [data-goto] elements are
409
+ // outlined and a click on one reports goto instead
410
+ // of reaching the page; a new document starts off
386
411
  //
387
412
  // Knobs (`/s/:id?k=`, validated by the route against the mock's declared knobs,
388
413
  // and the `knobs` command): for each path, dots become "-" in names, and
@@ -482,7 +507,8 @@ export const PARTS_JS = `
482
507
  parts: parts,
483
508
  height: document.body ? document.body.scrollHeight : document.documentElement.scrollHeight,
484
509
  scroll: { x: sx, y: sy },
485
- viewport: { w: innerWidth, h: innerHeight },
510
+ // scrollWidth past innerWidth: the design is wider than the frame.
511
+ viewport: { w: innerWidth, h: innerHeight, scrollW: document.documentElement.scrollWidth },
486
512
  };
487
513
  }
488
514
  var last = '';
@@ -552,12 +578,97 @@ export const PARTS_JS = `
552
578
  mo.takeRecords();
553
579
  }
554
580
 
581
+ // Try: the host lets the user click through the design; an element marked
582
+ // data-goto moves them to another screen. Only the host knows what that
583
+ // names, so the frame just reports it, and the click goes no further.
584
+ var trying = false;
585
+ function setTrying(on) {
586
+ trying = on;
587
+ document.documentElement.classList.toggle('mockpit-try', on);
588
+ mo.takeRecords();
589
+ }
590
+ window.addEventListener('click', function (e) {
591
+ if (!trying) return;
592
+ var t = e.target && e.target.closest ? e.target.closest('[data-goto]') : null;
593
+ if (!t) return;
594
+ e.preventDefault();
595
+ e.stopPropagation();
596
+ parent.postMessage({
597
+ __mockpit: true,
598
+ type: 'goto',
599
+ version: version,
600
+ to: text(t.getAttribute('data-goto')),
601
+ }, '*');
602
+ }, true);
603
+
604
+ // A finger's tap, in document px, for a host that lets touches through to the
605
+ // page (its canvas, where the page scrolls natively). A drag is a scroll: the
606
+ // browser cancels the pointer or it moves too far. A mouse clicks the host's
607
+ // own overlay instead, so it never reports here.
608
+ var tapAt = null;
609
+ window.addEventListener('pointerdown', function (e) {
610
+ tapAt = e.isPrimary && e.pointerType !== 'mouse' ? { x: e.clientX, y: e.clientY, t: Date.now() } : null;
611
+ }, true);
612
+ window.addEventListener('pointercancel', function () { tapAt = null; }, true);
613
+ window.addEventListener('pointerup', function (e) {
614
+ var a = tapAt;
615
+ tapAt = null;
616
+ if (!a || !e.isPrimary) return;
617
+ if (Math.abs(e.clientX - a.x) > 10 || Math.abs(e.clientY - a.y) > 10 || Date.now() - a.t > 600) return;
618
+ parent.postMessage({
619
+ __mockpit: true,
620
+ type: 'tap',
621
+ x: e.clientX + (window.scrollX || 0),
622
+ y: e.clientY + (window.scrollY || 0),
623
+ }, '*');
624
+ }, true);
625
+
626
+ // Two fingers are the host's: a page laid on its canvas takes one-finger
627
+ // scrolls itself, but a pinch zooms the canvas, not this page or the browser.
628
+ var pinching = false;
629
+ function pinch(phase, e) {
630
+ var t = e.touches;
631
+ parent.postMessage({
632
+ __mockpit: true,
633
+ type: 'pinch',
634
+ phase: phase,
635
+ a: t[0] ? { x: t[0].clientX, y: t[0].clientY } : null,
636
+ b: t[1] ? { x: t[1].clientX, y: t[1].clientY } : null,
637
+ }, '*');
638
+ }
639
+ window.addEventListener('touchstart', function (e) {
640
+ if (e.touches.length !== 2) return;
641
+ e.preventDefault();
642
+ pinching = true;
643
+ pinch('start', e);
644
+ }, { capture: true, passive: false });
645
+ window.addEventListener('touchmove', function (e) {
646
+ if (!pinching || e.touches.length < 2) return;
647
+ e.preventDefault();
648
+ pinch('move', e);
649
+ }, { capture: true, passive: false });
650
+ function release(e) {
651
+ if (!pinching || e.touches.length >= 2) return;
652
+ pinching = false;
653
+ pinch('end', e);
654
+ }
655
+ window.addEventListener('touchend', release, true);
656
+ window.addEventListener('touchcancel', release, true);
657
+ // ⌘/Ctrl+scroll and a trackpad pinch (ctrl+wheel) are the host's too: they
658
+ // zoom the canvas around the pointer, never this page or the browser.
659
+ window.addEventListener('wheel', function (e) {
660
+ if (!e.ctrlKey && !e.metaKey) return;
661
+ e.preventDefault();
662
+ parent.postMessage({ __mockpit: true, type: 'zoom', x: e.clientX, y: e.clientY, dy: e.deltaY }, '*');
663
+ }, { capture: true, passive: false });
664
+
555
665
  window.addEventListener('message', function (e) {
556
666
  if (e.source !== parent) return;
557
667
  var d = e.data;
558
668
  if (!d || d.__mockpit !== true) return;
559
669
  if (d.type === 'highlight') setHighlight(Array.isArray(d.parts) ? d.parts : []);
560
670
  else if (d.type === 'clear') setHighlight(null);
671
+ else if (d.type === 'try') setTrying(d.on === true);
561
672
  else if (d.type === 'scroll') window.scrollBy(Number(d.dx) || 0, Number(d.dy) || 0);
562
673
  else if (d.type === 'hit') {
563
674
  var el = document.elementFromPoint(
@@ -612,9 +723,19 @@ ${KNOB_VALUE_JS}
612
723
  detail: { values: JSON.parse(JSON.stringify(current)) },
613
724
  }));
614
725
  }
615
- function initial() { apply(doc.knobs && typeof doc.knobs === 'object' ? doc.knobs : {}, false); }
726
+ // The URL's values are already on <html>; a live command may also have come
727
+ // in before the body existed, so both are bound once it does.
728
+ function initial() {
729
+ var start = doc.knobs && typeof doc.knobs === 'object' ? doc.knobs : {};
730
+ for (var k in start) if (own(start, k) && !own(current, k)) current[k] = start[k];
731
+ apply(current, false);
732
+ }
616
733
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initial);
617
734
  else initial();
735
+ // Runs from <head>, before the page's own markup and anything slow it loads
736
+ // (a web font, a CDN stylesheet), so the host can send values at once rather
737
+ // than wait for the first parts report, which those can hold back.
738
+ if (parent !== window) parent.postMessage({ __mockpit: true, type: 'ready', version: doc.version }, '*');
618
739
  window.addEventListener('message', function (e) {
619
740
  if (e.source !== parent || parent === window) return;
620
741
  var d = e.data;
@@ -680,6 +801,16 @@ const scriptJson = (v) => JSON.stringify(v)
680
801
  // knobCss (quoted unless plain tokens) and then escapeHtml as the style
681
802
  // attribute, and the JSON through scriptJson.
682
803
  export function knobPreamble(version, knobs) {
804
+ const { attrs, values } = knobAttributes(knobs);
805
+ return {
806
+ htmlTag: `<html lang="en"${attrs}>`,
807
+ headScript: `<script>window.__mockpitDoc=${scriptJson({ version: version ?? null, knobs: values })};</script>`,
808
+ };
809
+ }
810
+ // Knob values as an element's `data-k-*` attributes and `--k-*` style vars, the
811
+ // same marks the preamble puts on <html>; an embedded component's wrapper
812
+ // carries its own settled values this way.
813
+ export function knobAttributes(knobs) {
683
814
  const attrs = [];
684
815
  const vars = [];
685
816
  const values = {};
@@ -694,14 +825,11 @@ export function knobPreamble(version, knobs) {
694
825
  vars.push(`--k-${name}:${css}`);
695
826
  }
696
827
  const style = vars.length ? ` style="${escapeHtml(vars.join(";"))}"` : "";
697
- return {
698
- htmlTag: `<html lang="en"${attrs.join("")}${style}>`,
699
- headScript: `<script>window.__mockpitDoc=${scriptJson({ version: version ?? null, knobs: values })};</script>`,
700
- };
828
+ return { attrs: `${attrs.join("")}${style}`, values };
701
829
  }
702
830
  // Our own highlight, shipped in the base stylesheet so turning it on is a class
703
831
  // flip rather than a DOM insertion the parts observer would report.
704
- const PART_HIGHLIGHT_CSS = `.mockpit-part-hl{outline:2px solid #8b7bff !important;outline-offset:2px}`;
832
+ const PART_HIGHLIGHT_CSS = `.mockpit-part-hl{outline:2px solid #8b7bff !important;outline-offset:2px}html.mockpit-try [data-goto]{outline:2px dashed #3fb27f !important;outline-offset:3px;cursor:pointer}`;
705
833
  // Tailwind's browser build, pinned to a major (jsdelivr is already on the CDN
706
834
  // allowlist, so this needs no CSP widening). Injected only when a project's
707
835
  // design settings say `kit: "tailwind"`, i.e. the repo itself is a Tailwind
@@ -1074,6 +1202,9 @@ export function renderHtmlPage(doc) {
1074
1202
  ]),
1075
1203
  `<style>${design.css}${colorSchemeCss(doc.mode)}</style>`,
1076
1204
  ].join("\n");
1205
+ // Slots (fills.ts): only the chosen fill of each shows, keyed on the knob
1206
+ // attribute; with no value yet, the default fill.
1207
+ const fillCss = slotCss(scanFills(doc.html).slots);
1077
1208
  return `<!doctype html>
1078
1209
  ${htmlTag}
1079
1210
  <head>
@@ -1082,7 +1213,9 @@ ${htmlTag}
1082
1213
  <meta http-equiv="Content-Security-Policy" content="${buildCsp(doc.origin)}">
1083
1214
  <title>${escapeHtml(doc.title)}</title>
1084
1215
  ${preamble.headScript}
1216
+ ${scriptTag(doc.origin, KNOBS_PATH)}
1085
1217
  ${styles}
1218
+ ${fillCss ? `<style>${fillCss}</style>` : ""}
1086
1219
  ${design.headScripts}
1087
1220
  ${kits.flatMap((k) => (k.script ? [`<script src="${escapeHtml(k.script)}" defer></script>`] : [])).join("\n")}
1088
1221
  </head>
@@ -1091,7 +1224,6 @@ ${SVG_DEFS}
1091
1224
  ${expandIcons(doc.html, doc.resolveIcon ?? (() => null)).html}
1092
1225
  ${scriptTag(doc.origin, BRIDGE_PATH)}
1093
1226
  ${scriptTag(doc.origin, HIT_TEST_PATH)}
1094
- ${scriptTag(doc.origin, KNOBS_PATH)}
1095
1227
  ${scriptTag(doc.origin, PARTS_PATH)}
1096
1228
  ${kit.js ? `<script>${kit.js}</script>` : ""}
1097
1229
  </body>
@@ -2,6 +2,8 @@ export interface Accent {
2
2
  bg: string;
3
3
  text: string;
4
4
  border: string;
5
+ solid?: string;
6
+ onSolid?: string;
5
7
  }
6
8
  export interface Palette {
7
9
  bg: string;
@@ -21,6 +21,8 @@ function viewerVars(p) {
21
21
  "border-2": p.border2,
22
22
  accent: p.info.text,
23
23
  "accent-bg": p.info.bg,
24
+ "accent-solid": p.info.solid ?? p.info.text,
25
+ "on-accent": p.info.onSolid ?? "#fff",
24
26
  hover: p.hover,
25
27
  danger: p.danger.text,
26
28
  };
@@ -111,7 +113,14 @@ export const DIALKIT = {
111
113
  border: "#e6e6e6",
112
114
  border2: "#d6d6d6",
113
115
  hover: "#ebebeb",
114
- info: { bg: "rgba(37, 99, 235, 0.1)", text: "#2563eb", border: "#2563eb" },
116
+ // White on #2563eb is already 5.2:1, so the fill is the accent itself.
117
+ info: {
118
+ bg: "rgba(37, 99, 235, 0.1)",
119
+ text: "#2563eb",
120
+ border: "#2563eb",
121
+ solid: "#2563eb",
122
+ onSolid: "#fff",
123
+ },
115
124
  success: { bg: "rgba(43, 147, 72, 0.12)", text: "#2b9348", border: "#2b9348" },
116
125
  warning: { bg: "#fff8e6", text: "#e8590c", border: "#f0e0b8" },
117
126
  danger: { bg: "rgba(220, 38, 38, 0.1)", text: "#dc2626", border: "#dc2626" },
@@ -126,7 +135,14 @@ export const DIALKIT = {
126
135
  border: "#242424",
127
136
  border2: "#3a3a3a",
128
137
  hover: "#262626",
129
- info: { bg: "rgba(139, 123, 255, 0.16)", text: "#8b7bff", border: "#8b7bff" },
138
+ // #8b7bff under white text is 3.3:1; the same hue, darker, is 5.0:1.
139
+ info: {
140
+ bg: "rgba(139, 123, 255, 0.16)",
141
+ text: "#8b7bff",
142
+ border: "#8b7bff",
143
+ solid: "#6b5ae0",
144
+ onSolid: "#fff",
145
+ },
130
146
  success: { bg: "rgba(74, 222, 128, 0.14)", text: "#4ade80", border: "#4ade80" },
131
147
  warning: { bg: "#2a2312", text: "#e8590c", border: "#4a3c1c" },
132
148
  danger: { bg: "rgba(248, 113, 113, 0.14)", text: "#f87171", border: "#f87171" },
@@ -238,10 +254,14 @@ function pick(vars, names) {
238
254
  return null;
239
255
  }
240
256
  function accentFrom(vars, bg, text, border, fallback) {
257
+ const ink = pick(vars, text);
241
258
  return {
242
259
  bg: pick(vars, bg) ?? fallback.bg,
243
- text: pick(vars, text) ?? fallback.text,
260
+ text: ink ?? fallback.text,
244
261
  border: pick(vars, border) ?? fallback.border,
262
+ // The default's solid pair belongs to the default's ink; a repo's own ink
263
+ // fills with itself.
264
+ ...(ink ? {} : { solid: fallback.solid, onSolid: fallback.onSolid }),
245
265
  };
246
266
  }
247
267
  function paletteFrom(vars, fallback) {
@@ -197,14 +197,22 @@ export interface PadValue {
197
197
  }
198
198
  export type KnobValue = number | boolean | string | PadValue | SpringKnob | EasingKnob;
199
199
  export type MockKind = "page" | "component";
200
- export type PostStatus = "open" | "accepted" | "archived";
200
+ export type PostStatus = "open" | "accepted" | "archived" | "dropped";
201
+ export declare const isLive: (status: PostStatus) => boolean;
202
+ export declare const inUseVersion: (post: Pick<Post, "status" | "version" | "acceptedVersion">) => number;
203
+ export declare function postAtVersion(post: Post, version: number): Post | null;
204
+ export declare function versionAt(post: Pick<Post, "version" | "history">, n: number): number;
201
205
  export interface AskOption {
202
206
  id: string;
203
207
  label: string;
204
208
  variant?: string;
209
+ version?: number;
205
210
  set?: Record<string, KnobValue>;
206
211
  }
207
212
  export type AskAnswer = string | string[];
213
+ export declare const optionState: (ask: Pick<Ask, "state">, mock: {
214
+ states: string[];
215
+ }) => string | null;
208
216
  export interface Ask {
209
217
  id: string;
210
218
  text: string;
@@ -213,11 +221,13 @@ export interface Ask {
213
221
  part?: string;
214
222
  options: AskOption[];
215
223
  multi?: boolean;
224
+ after?: string;
216
225
  answer?: AskAnswer;
217
226
  other?: string;
218
227
  note?: string;
219
228
  otherImages?: string[];
220
229
  noteImages?: string[];
230
+ dismissed?: true;
221
231
  at: string;
222
232
  }
223
233
  export declare const OTHER_ID = "other";
@@ -242,6 +252,7 @@ export interface ReplyDecision {
242
252
  kind: "accept" | "revise" | "drop";
243
253
  state: string | null;
244
254
  variant: string;
255
+ version?: number;
245
256
  }
246
257
  export interface Draft {
247
258
  version: number;
@@ -268,6 +279,9 @@ export interface Reply {
268
279
  noteImages?: Record<string, string[]>;
269
280
  text?: string;
270
281
  decision?: ReplyDecision;
282
+ resolved?: {
283
+ dismissed: string[];
284
+ };
271
285
  }
272
286
  export interface Mock {
273
287
  id: string;
@@ -278,15 +292,18 @@ export interface Mock {
278
292
  states: string[];
279
293
  asks: Ask[];
280
294
  knobs: Knobs;
295
+ widths: number[];
281
296
  draft: Draft | null;
282
297
  sessionId: string | null;
298
+ resolvedAt: string | null;
283
299
  createdAt: string;
284
300
  updatedAt: string;
285
301
  }
286
- export interface Slot {
302
+ export interface Embed {
287
303
  slug: string;
288
304
  variant: string;
289
305
  version: number;
306
+ pinned?: true;
290
307
  }
291
308
  export interface PostVersion {
292
309
  version: number;
@@ -297,6 +314,10 @@ export interface PostVersion {
297
314
  prompt?: string;
298
315
  author?: string;
299
316
  }
317
+ export interface ForkRef {
318
+ variant: string;
319
+ version: number;
320
+ }
300
321
  export interface Post {
301
322
  id: string;
302
323
  sessionId: string;
@@ -311,11 +332,28 @@ export interface Post {
311
332
  version: number;
312
333
  history: PostVersion[];
313
334
  knobs?: Knobs;
314
- slots: Slot[];
335
+ note?: string;
336
+ embeds: Embed[];
337
+ forkedFrom?: ForkRef;
338
+ starred?: true;
339
+ acceptedVersion?: number;
340
+ hiddenVersions?: number[];
315
341
  from?: number;
316
342
  prompt?: string;
317
343
  author?: string;
318
344
  }
345
+ export interface FlowStep {
346
+ mock: string;
347
+ state: string | null;
348
+ }
349
+ export interface Flow {
350
+ project: string;
351
+ name: string;
352
+ steps: FlowStep[];
353
+ createdAt: string;
354
+ updatedAt: string;
355
+ }
356
+ export declare const sameScreen: (a: FlowStep, b: FlowStep) => boolean;
319
357
  export interface ProjectSummary {
320
358
  name: string;
321
359
  mocks: number;
@@ -470,6 +508,7 @@ export interface CreateMockInput {
470
508
  kind?: MockKind;
471
509
  states?: string[];
472
510
  knobs?: Knobs;
511
+ widths?: number[];
473
512
  sessionId?: string | null;
474
513
  }
475
514
  export interface UpdateMockInput {
@@ -478,7 +517,9 @@ export interface UpdateMockInput {
478
517
  states?: string[];
479
518
  asks?: Ask[];
480
519
  knobs?: Knobs;
520
+ widths?: number[];
481
521
  sessionId?: string | null;
522
+ resolvedAt?: string | null;
482
523
  }
483
524
  export interface CreatePostInput {
484
525
  sessionId: string;
@@ -488,7 +529,9 @@ export interface CreatePostInput {
488
529
  title?: string;
489
530
  surfaces: Surface[];
490
531
  knobs?: Knobs;
491
- slots?: Slot[];
532
+ note?: string;
533
+ embeds?: Embed[];
534
+ forkedFrom?: ForkRef;
492
535
  from?: number;
493
536
  prompt?: string;
494
537
  author?: string;
@@ -497,7 +540,8 @@ export interface UpdatePostInput {
497
540
  title?: string;
498
541
  surfaces?: Surface[];
499
542
  knobs?: Knobs | null;
500
- slots?: Slot[];
543
+ note?: string;
544
+ embeds?: Embed[];
501
545
  from?: number;
502
546
  prompt?: string;
503
547
  author?: string;
@@ -528,8 +572,14 @@ export interface CommitReplyInput {
528
572
  text: string;
529
573
  payload: Reply;
530
574
  asks: Ask[];
531
- accept: string[];
575
+ accept: {
576
+ id: string;
577
+ version: number;
578
+ }[];
532
579
  archive: string[];
580
+ drop?: string[];
581
+ resolvedAt?: string;
582
+ keepDraft?: true;
533
583
  }
534
584
  export interface PostQuery {
535
585
  mockId?: string;
@@ -554,6 +604,8 @@ export interface Store {
554
604
  createMock(input: CreateMockInput): Promise<Mock>;
555
605
  updateMock(id: string, patch: UpdateMockInput): Promise<Mock | null>;
556
606
  removeMock(id: string): Promise<boolean>;
607
+ listFlows(project: string): Promise<Flow[]>;
608
+ placeFlowStep(project: string, name: string, screen: FlowStep, step: number): Promise<Flow | null>;
557
609
  putDraft(mockId: string, draft: Draft | null): Promise<Mock | null>;
558
610
  listPosts(query?: PostQuery): Promise<Post[]>;
559
611
  countPostsBySession(): Promise<Map<string, number>>;
@@ -562,7 +614,9 @@ export interface Store {
562
614
  createPost(input: CreatePostInput): Promise<Post | null>;
563
615
  updatePost(id: string, patch: UpdatePostInput): Promise<Post | null>;
564
616
  removePost(id: string): Promise<boolean>;
565
- setPostStatus(id: string, status: PostStatus): Promise<Post | null>;
617
+ setPostStatus(id: string, status: PostStatus, acceptedVersion?: number): Promise<Post | null>;
618
+ setPostStarred(id: string, starred: boolean): Promise<Post | null>;
619
+ setVersionHidden(id: string, version: number, hidden: boolean): Promise<Post | null>;
566
620
  listComments(query: CommentQuery): Promise<Comment[]>;
567
621
  createComment(input: CreateCommentInput): Promise<Comment | null>;
568
622
  removeComment(id: string): Promise<Comment | null>;
@@ -600,10 +654,18 @@ export declare function projectFromCwd(cwd: string | null | undefined): string |
600
654
  export declare const htmlSurface: (html: string, kits?: unknown) => HtmlSurface;
601
655
  export declare const askAnswered: (a: Pick<Ask, "answer" | "other" | "note">) => boolean;
602
656
  export declare const openAsks: (mock: Pick<Mock, "asks">) => Ask[];
603
- export declare function builtinAskIds(mock: Pick<Mock, "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string): string[];
604
- export declare const builtinAskCount: (mock: Pick<Mock, "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string) => number;
605
- export declare const openCount: (mock: Pick<Mock, "id" | "asks" | "states">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]) => number;
606
- export declare function draftedCount(mock: Pick<Mock, "id" | "asks" | "states" | "draft">, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]): number | null;
657
+ export declare function builtinAskIds(mock: Pick<Mock, "asks" | "states"> & {
658
+ resolvedAt?: string | null;
659
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string): string[];
660
+ export declare const builtinAskCount: (mock: Pick<Mock, "asks" | "states"> & {
661
+ resolvedAt?: string | null;
662
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[], mockId: string) => number;
663
+ export declare const openCount: (mock: Pick<Mock, "id" | "asks" | "states"> & {
664
+ resolvedAt?: string | null;
665
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]) => number;
666
+ export declare function draftedCount(mock: Pick<Mock, "id" | "asks" | "states" | "draft"> & {
667
+ resolvedAt?: string | null;
668
+ }, posts: Pick<Post, "mock" | "state" | "variant" | "status">[]): number | null;
607
669
  export declare const BUILTIN_ASK_ID = "variant";
608
670
  export declare function builtinAsk(mock: Pick<Mock, "states">, posts: Pick<Post, "state" | "variant">[], id: string): Ask | undefined;
609
671
  export declare function summarizeProjects(mocks: Mock[], sessions: Session[], posts?: Post[]): ProjectSummary[];
@@ -51,6 +51,42 @@ export function isSurfaceKind(kind) {
51
51
  export function isSandboxedSurfaceKind(kind) {
52
52
  return isSurfaceKind(kind) && SURFACE_KIND_METADATA[kind].sandboxed;
53
53
  }
54
+ // Still in the running: what the user can choose between.
55
+ export const isLive = (status) => status === "open" || status === "accepted";
56
+ // The version a variant is resolved at when nothing names one: the version in
57
+ // use when accepted, else its latest.
58
+ export const inUseVersion = (post) => post.status === "accepted" ? (post.acceptedVersion ?? post.version) : post.version;
59
+ // A variant as it stood at one of its versions (current or retained), or null
60
+ // when that version is gone.
61
+ export function postAtVersion(post, version) {
62
+ if (version === post.version)
63
+ return post;
64
+ const old = post.history.find((h) => h.version === version);
65
+ if (!old)
66
+ return null;
67
+ const { from: _f, prompt: _p, author: _a, ...rest } = post;
68
+ return {
69
+ ...rest,
70
+ version: old.version,
71
+ title: old.title,
72
+ surfaces: old.surfaces,
73
+ updatedAt: old.at,
74
+ ...(old.from === undefined ? {} : { from: old.from }),
75
+ ...(old.prompt === undefined ? {} : { prompt: old.prompt }),
76
+ ...(old.author === undefined ? {} : { author: old.author }),
77
+ };
78
+ }
79
+ // The variant's newest version not after mock version `n` — what the stage
80
+ // showed when a draft or reply was bound to n (the viewer's frameVersion).
81
+ export function versionAt(post, n) {
82
+ const versions = [post.version, ...post.history.map((h) => h.version)];
83
+ const fit = versions.filter((v) => v <= n);
84
+ return fit.length ? Math.max(...fit) : Math.min(...versions);
85
+ }
86
+ // The state a version option names a version in: the ask's state, else the
87
+ // mock's first (`ask` accepts version options without a state only while the
88
+ // mock has one).
89
+ export const optionState = (ask, mock) => ask.state ?? mock.states[0] ?? null;
54
90
  // The viewer's write-in option on every ask. Reserved, so a delivered
55
91
  // `{id: "other", other: true}` choice never shadows an option the agent declared.
56
92
  export const OTHER_ID = "other";
@@ -58,6 +94,7 @@ export const OTHER_ID = "other";
58
94
  // for a reviewer on a phone who would rather not type: like, dislike, or
59
95
  // "more like this". Rides as a part comment, so it travels in the one reply.
60
96
  export const REACTIONS = ["up", "down", "more"];
97
+ export const sameScreen = (a, b) => a.mock === b.mock && a.state === b.state;
61
98
  export const HISTORY_LIMIT = 20;
62
99
  // "user" is the reserved trust label for genuine human comments. A session
63
100
  // agent with that name could otherwise have its programmatic comments delivered
@@ -152,15 +189,18 @@ export const htmlSurface = (html, kits) => ({
152
189
  // An open ask is one no reply has answered yet: a write-in or a note alone
153
190
  // answers it too ("Neither, because…").
154
191
  export const askAnswered = (a) => a.answer !== undefined || a.other !== undefined || a.note !== undefined;
155
- export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a));
192
+ export const openAsks = (mock) => mock.asks.filter((a) => !askAnswered(a) && !a.dismissed);
156
193
  // How many built-in "Which one?" asks the viewer shows: one mock-wide when every
157
194
  // state needs one and the live names line up, else one per needing state. Same
158
195
  // rule as viewer/src/logic.ts builtinAsks, so Home, `read` and `pending` count
159
196
  // the question the user actually sees. Answering it archives the losers, which
160
197
  // is what makes it stop counting.
161
198
  export function builtinAskIds(mock, posts, mockId) {
199
+ // Resolving closed the choice along with every other open question.
200
+ if (mock.resolvedAt)
201
+ return [];
162
202
  const live = (state) => posts
163
- .filter((p) => p.mock === mockId && p.state === state && p.status !== "archived")
203
+ .filter((p) => p.mock === mockId && p.state === state && isLive(p.status))
164
204
  .map((p) => p.variant);
165
205
  const needs = (state) => {
166
206
  const names = live(state);
@@ -0,0 +1,2 @@
1
+ export declare const VIEWER_CLIENT = "viewer";
2
+ export declare function isViewerRequest(request: Request): boolean;
@@ -0,0 +1,14 @@
1
+ // Whether a request comes from the trusted viewer, the page this server
2
+ // served: only it may act as the user (drafts, replies, user comments).
3
+ // Fetch Metadata says so directly, but browsers omit it on plain http away
4
+ // from localhost (a LAN or tailnet address), where the viewer then looked
5
+ // like any other client and every draft call failed. The viewer also names
6
+ // itself with a custom header, which a cross-origin caller cannot send: that
7
+ // header forces a CORS preflight, and this server answers none, so a sandboxed
8
+ // surface (opaque origin) or another site is refused by the browser itself.
9
+ export const VIEWER_CLIENT = "viewer";
10
+ export function isViewerRequest(request) {
11
+ if (request.headers.get("sec-fetch-site") === "same-origin")
12
+ return true;
13
+ return request.headers.get("x-mockpit-client") === VIEWER_CLIENT;
14
+ }