mockpit 1.1.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 (62) hide show
  1. package/README.md +13 -6
  2. package/bin/mockpit.js +139 -18
  3. package/dist/mcp/server.js +30 -10
  4. package/dist/server/apiViews.d.ts +129 -25
  5. package/dist/server/apiViews.js +86 -23
  6. package/dist/server/app.d.ts +2 -1
  7. package/dist/server/app.js +1258 -200
  8. package/dist/server/appIcon.d.ts +22 -0
  9. package/dist/server/appIcon.js +43 -0
  10. package/dist/server/appIconPng.d.ts +1 -0
  11. package/dist/server/appIconPng.js +6 -0
  12. package/dist/server/askNudges.d.ts +3 -0
  13. package/dist/server/askNudges.js +40 -0
  14. package/dist/server/base64.d.ts +1 -0
  15. package/dist/server/base64.js +8 -0
  16. package/dist/server/decisions.d.ts +23 -0
  17. package/dist/server/decisions.js +99 -0
  18. package/dist/server/designGuide.js +53 -16
  19. package/dist/server/embeds.d.ts +25 -0
  20. package/dist/server/embeds.js +118 -0
  21. package/dist/server/eventLog.js +2 -1
  22. package/dist/server/feedbackBatch.d.ts +28 -5
  23. package/dist/server/feedbackBatch.js +78 -11
  24. package/dist/server/fills.d.ts +41 -0
  25. package/dist/server/fills.js +328 -0
  26. package/dist/server/handoff.d.ts +6 -0
  27. package/dist/server/handoff.js +38 -0
  28. package/dist/server/index.js +15 -1
  29. package/dist/server/knobs.d.ts +6 -2
  30. package/dist/server/knobs.js +13 -4
  31. package/dist/server/mcpHttp.d.ts +5 -2
  32. package/dist/server/mcpHttp.js +14 -5
  33. package/dist/server/mcpSpec.d.ts +21 -13
  34. package/dist/server/mcpSpec.js +99 -9
  35. package/dist/server/postSurfaces.js +3 -12
  36. package/dist/server/richRender.js +3 -1
  37. package/dist/server/run.js +6 -2
  38. package/dist/server/runApi.d.ts +1 -1
  39. package/dist/server/runApi.js +5 -4
  40. package/dist/server/sqlStore.d.ts +92 -5
  41. package/dist/server/sqlStore.js +188 -30
  42. package/dist/server/surfacePage.d.ts +6 -2
  43. package/dist/server/surfacePage.js +142 -10
  44. package/dist/server/themes.d.ts +2 -0
  45. package/dist/server/themes.js +23 -3
  46. package/dist/server/types.d.ts +95 -10
  47. package/dist/server/types.js +89 -6
  48. package/dist/server/viewerRequest.d.ts +2 -0
  49. package/dist/server/viewerRequest.js +14 -0
  50. package/extensions/mockpit.js +1 -0
  51. package/guide/topics/asks.md +69 -10
  52. package/guide/topics/feedback.md +104 -16
  53. package/guide/topics/html.md +5 -1
  54. package/guide/topics/http.md +2 -1
  55. package/guide/topics/knobs.md +4 -3
  56. package/guide/topics/surfaces.md +32 -16
  57. package/package.json +4 -13
  58. package/skills/launch-video/SKILL.md +1 -1
  59. package/skills/mockpit/SKILL.md +14 -4
  60. package/viewer/dist/index.html +9 -9
  61. package/dist/server/slots.d.ts +0 -14
  62. package/dist/server/slots.js +0 -71
@@ -1,4 +1,4 @@
1
- import { type Ask, type Asset, type Comment, type CommentQuery, type CommitReplyInput, type CreateAssetInput, type CreateCommentInput, type CreateMockInput, type CreatePostInput, type CreateSessionInput, type Draft, type Knobs, type Mock, type MockKind, type Post, type PostQuery, type PostStatus, type PostVersion, type Session, type Slot, type SqlStorage, type Store, type Surface, type UpdateMockInput, type UpdatePostInput } from "./types.ts";
1
+ import { type Ask, type Asset, type Comment, type CommentQuery, type CommitReplyInput, type CreateAssetInput, type CreateCommentInput, type CreateMockInput, type CreatePostInput, type CreateSessionInput, type Draft, type Knobs, type Mock, type MockKind, type Post, type PostQuery, type PostStatus, type PostVersion, type Session, type Embed, type Flow, type FlowStep, type ForkRef, type SqlStorage, type Store, type Surface, type UpdateMockInput, type UpdatePostInput } from "./types.ts";
2
2
  export declare class SqlStore implements Store {
3
3
  private sql;
4
4
  private assetRefCache;
@@ -26,7 +26,7 @@ export declare class SqlStore implements Store {
26
26
  private rowToPost;
27
27
  private rowToAsset;
28
28
  private rowToComment;
29
- listSessions(): Promise<Session[]>;
29
+ listSessions(project?: string): Promise<Session[]>;
30
30
  getSession(id: string): Promise<Session | null>;
31
31
  getSessionByKey(key: string): Promise<Session | null>;
32
32
  createSession(input: CreateSessionInput): Promise<Session>;
@@ -34,6 +34,8 @@ export declare class SqlStore implements Store {
34
34
  removeSession(id: string): Promise<boolean>;
35
35
  private touch;
36
36
  markAgentSeen(sessionId: string, seq: number): Promise<void>;
37
+ touchAgent(sessionId: string): Promise<void>;
38
+ handOff(sessionId: string, seen: Pick<Session, "agentSeq" | "agentActiveAt">, toSeq: number): Promise<boolean>;
37
39
  getSetting(key: string): Promise<string | null>;
38
40
  setSetting(key: string, value: string): Promise<void>;
39
41
  listProjects(): Promise<import("./types.ts").ProjectSummary[]>;
@@ -43,6 +45,10 @@ export declare class SqlStore implements Store {
43
45
  createMock(input: CreateMockInput): Promise<Mock>;
44
46
  updateMock(id: string, patch: UpdateMockInput): Promise<Mock | null>;
45
47
  removeMock(id: string): Promise<boolean>;
48
+ private flowsOf;
49
+ private writeFlow;
50
+ listFlows(project: string): Promise<Flow[]>;
51
+ placeFlowStep(project: string, name: string, screen: FlowStep, step: number): Promise<Flow | null>;
46
52
  putDraft(mockId: string, draft: Draft | null): Promise<{
47
53
  draft: Draft | null;
48
54
  id: string;
@@ -53,7 +59,9 @@ export declare class SqlStore implements Store {
53
59
  states: string[];
54
60
  asks: Ask[];
55
61
  knobs: Knobs;
62
+ widths: number[];
56
63
  sessionId: string | null;
64
+ resolvedAt: string | null;
57
65
  createdAt: string;
58
66
  updatedAt: string;
59
67
  } | null>;
@@ -65,13 +73,14 @@ export declare class SqlStore implements Store {
65
73
  createPost(input: CreatePostInput): Promise<Post | null>;
66
74
  updatePost(id: string, patch: UpdatePostInput): Promise<{
67
75
  author?: string | undefined;
68
- slots: Slot[];
76
+ embeds: Embed[];
69
77
  state: string | null;
70
78
  version: number;
71
79
  updatedAt: string;
72
80
  history: PostVersion[];
73
81
  from: number;
74
82
  prompt: string;
83
+ note?: string | undefined;
75
84
  knobs?: Knobs | undefined;
76
85
  title: string;
77
86
  surfaces: Surface[];
@@ -81,9 +90,13 @@ export declare class SqlStore implements Store {
81
90
  variant: string;
82
91
  status: PostStatus;
83
92
  createdAt: string;
93
+ forkedFrom?: ForkRef;
94
+ starred?: true;
95
+ acceptedVersion?: number;
96
+ hiddenVersions?: number[];
84
97
  } | null>;
85
98
  removePost(id: string): Promise<boolean>;
86
- setPostStatus(id: string, status: PostStatus): Promise<{
99
+ setPostStatus(id: string, status: PostStatus, acceptedVersion?: number): Promise<{
87
100
  status: PostStatus;
88
101
  id: string;
89
102
  sessionId: string;
@@ -97,11 +110,85 @@ export declare class SqlStore implements Store {
97
110
  version: number;
98
111
  history: PostVersion[];
99
112
  knobs?: Knobs;
100
- slots: Slot[];
113
+ note?: string;
114
+ embeds: Embed[];
115
+ forkedFrom?: ForkRef;
116
+ starred?: true;
117
+ hiddenVersions?: number[];
118
+ from?: number;
119
+ prompt?: string;
120
+ author?: string;
121
+ } | {
122
+ status: PostStatus;
123
+ acceptedVersion: number;
124
+ id: string;
125
+ sessionId: string;
126
+ mock: string;
127
+ state: string | null;
128
+ variant: string;
129
+ title: string;
130
+ surfaces: Surface[];
131
+ createdAt: string;
132
+ updatedAt: string;
133
+ version: number;
134
+ history: PostVersion[];
135
+ knobs?: Knobs;
136
+ note?: string;
137
+ embeds: Embed[];
138
+ forkedFrom?: ForkRef;
139
+ starred?: true;
140
+ hiddenVersions?: number[];
141
+ from?: number;
142
+ prompt?: string;
143
+ author?: string;
144
+ } | null>;
145
+ setPostStarred(id: string, starred: boolean): Promise<{
146
+ id: string;
147
+ sessionId: string;
148
+ mock: string;
149
+ state: string | null;
150
+ variant: string;
151
+ status: PostStatus;
152
+ title: string;
153
+ surfaces: Surface[];
154
+ createdAt: string;
155
+ updatedAt: string;
156
+ version: number;
157
+ history: PostVersion[];
158
+ knobs?: Knobs;
159
+ note?: string;
160
+ embeds: Embed[];
161
+ forkedFrom?: ForkRef;
162
+ acceptedVersion?: number;
163
+ hiddenVersions?: number[];
164
+ from?: number;
165
+ prompt?: string;
166
+ author?: string;
167
+ } | {
168
+ starred: true;
169
+ id: string;
170
+ sessionId: string;
171
+ mock: string;
172
+ state: string | null;
173
+ variant: string;
174
+ status: PostStatus;
175
+ title: string;
176
+ surfaces: Surface[];
177
+ createdAt: string;
178
+ updatedAt: string;
179
+ version: number;
180
+ history: PostVersion[];
181
+ knobs?: Knobs;
182
+ note?: string;
183
+ embeds: Embed[];
184
+ forkedFrom?: ForkRef;
185
+ acceptedVersion?: number;
186
+ hiddenVersions?: number[];
101
187
  from?: number;
102
188
  prompt?: string;
103
189
  author?: string;
104
190
  } | null>;
191
+ setVersionHidden(id: string, version: number, hidden: boolean): Promise<Post | null>;
105
192
  listComments(query: CommentQuery): Promise<Comment[]>;
106
193
  private insertComment;
107
194
  createComment(input: CreateCommentInput): Promise<Comment | null>;
@@ -1,4 +1,4 @@
1
- import { collectAssetIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, newId, normalizeSurfaceIds, projectFromCwd, reservedAgent, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
1
+ import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, newId, normalizeSurfaceIds, projectFromCwd, reservedAgent, sameScreen, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
2
2
  function parseJson(raw, fallback) {
3
3
  if (typeof raw !== "string" || !raw)
4
4
  return fallback;
@@ -14,7 +14,7 @@ const POST_COLUMNS = `
14
14
  variant TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open', title TEXT NOT NULL,
15
15
  surfaces TEXT NOT NULL, knobs TEXT, slots TEXT NOT NULL DEFAULT '[]',
16
16
  createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL, version INTEGER NOT NULL,
17
- curFrom INTEGER, curPrompt TEXT, curAuthor TEXT`;
17
+ curFrom INTEGER, curPrompt TEXT, curAuthor TEXT, note TEXT`;
18
18
  // Store implementation on SQLite — a Durable Object's `ctx.storage.sql` in the
19
19
  // Worker, or node:sqlite via an adapter on Node (see server/sqliteStorage.ts).
20
20
  // One workspace = one database, so plain SQL with no tenant columns.
@@ -65,13 +65,14 @@ export class SqlStore {
65
65
  CREATE TABLE IF NOT EXISTS sessions (
66
66
  id TEXT PRIMARY KEY, agent TEXT NOT NULL, title TEXT, cwd TEXT,
67
67
  createdAt TEXT NOT NULL, lastActiveAt TEXT NOT NULL,
68
- agentSeq INTEGER NOT NULL DEFAULT 0, project TEXT, key TEXT
68
+ agentSeq INTEGER NOT NULL DEFAULT 0, project TEXT, key TEXT, agentActiveAt TEXT
69
69
  );
70
70
  CREATE TABLE IF NOT EXISTS mocks (
71
71
  id TEXT PRIMARY KEY, project TEXT NOT NULL, slug TEXT NOT NULL, title TEXT NOT NULL,
72
72
  kind TEXT NOT NULL DEFAULT 'component', states TEXT NOT NULL DEFAULT '[]',
73
73
  asks TEXT NOT NULL DEFAULT '[]', knobs TEXT NOT NULL DEFAULT '{}', draft TEXT,
74
- sessionId TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL
74
+ sessionId TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
75
+ widths TEXT NOT NULL DEFAULT '[]'
75
76
  );
76
77
  CREATE TABLE IF NOT EXISTS posts (${POST_COLUMNS});
77
78
  CREATE TABLE IF NOT EXISTS post_versions (
@@ -95,14 +96,34 @@ export class SqlStore {
95
96
  CREATE TABLE IF NOT EXISTS settings (
96
97
  key TEXT PRIMARY KEY, value TEXT NOT NULL
97
98
  );
99
+ CREATE TABLE IF NOT EXISTS flows (
100
+ project TEXT NOT NULL, name TEXT NOT NULL, steps TEXT NOT NULL DEFAULT '[]',
101
+ createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
102
+ PRIMARY KEY (project, name)
103
+ );
98
104
  `);
99
105
  this.addMissing("sessions", {
100
106
  agentSeq: "INTEGER NOT NULL DEFAULT 0",
101
107
  project: "TEXT",
102
108
  key: "TEXT",
109
+ agentActiveAt: "TEXT",
103
110
  });
111
+ this.addMissing("mocks", { widths: "TEXT NOT NULL DEFAULT '[]'", resolvedAt: "TEXT" });
104
112
  if (legacy)
105
113
  this.migrateLegacy();
114
+ // After the legacy lift, which rebuilds posts from POST_COLUMNS alone.
115
+ const pinned = this.columns("posts").has("acceptedVersion");
116
+ this.addMissing("posts", {
117
+ note: "TEXT",
118
+ forkedFrom: "TEXT",
119
+ starred: "INTEGER NOT NULL DEFAULT 0",
120
+ acceptedVersion: "INTEGER",
121
+ hiddenVersions: "TEXT",
122
+ });
123
+ // Before the pin, accepted meant "its latest": freeze that as the version in use.
124
+ if (!pinned) {
125
+ this.sql.exec("UPDATE posts SET acceptedVersion = version WHERE status = 'accepted'");
126
+ }
106
127
  this.createIndexes();
107
128
  }
108
129
  createIndexes() {
@@ -381,6 +402,8 @@ export class SqlStore {
381
402
  lastActiveAt: r.lastActiveAt,
382
403
  agentSeq: r.agentSeq ?? 0,
383
404
  project: r.project ?? null,
405
+ agentActiveAt: r.agentActiveAt ?? null,
406
+ keyed: r.key != null,
384
407
  };
385
408
  }
386
409
  rowToMock(r) {
@@ -393,14 +416,17 @@ export class SqlStore {
393
416
  states: parseJson(r.states, []),
394
417
  asks: parseJson(r.asks, []),
395
418
  knobs: parseJson(r.knobs, {}),
419
+ widths: parseJson(r.widths, []),
396
420
  draft: parseJson(r.draft, null),
397
421
  sessionId: r.sessionId ?? null,
422
+ resolvedAt: r.resolvedAt ?? null,
398
423
  createdAt: r.createdAt,
399
424
  updatedAt: r.updatedAt,
400
425
  };
401
426
  }
402
427
  rowToPost(r, history) {
403
428
  const knobs = parseJson(r.knobs, null);
429
+ const hidden = parseJson(r.hiddenVersions, []);
404
430
  return {
405
431
  id: r.id,
406
432
  sessionId: r.sessionId,
@@ -415,7 +441,15 @@ export class SqlStore {
415
441
  version: r.version,
416
442
  history,
417
443
  ...(knobs ? { knobs } : {}),
418
- slots: parseJson(r.slots, []),
444
+ ...(r.note == null ? {} : { note: r.note }),
445
+ // The column keeps its first name, from when embeds were called slots.
446
+ embeds: parseJson(r.slots, []),
447
+ ...(r.forkedFrom == null ? {} : { forkedFrom: parseJson(r.forkedFrom, null) }),
448
+ ...(r.starred ? { starred: true } : {}),
449
+ ...(r.status === "accepted" && r.acceptedVersion != null
450
+ ? { acceptedVersion: r.acceptedVersion }
451
+ : {}),
452
+ ...(hidden.length ? { hiddenVersions: hidden } : {}),
419
453
  ...(r.curFrom == null ? {} : { from: r.curFrom }),
420
454
  ...(r.curPrompt == null ? {} : { prompt: r.curPrompt }),
421
455
  ...(r.curAuthor == null ? {} : { author: r.curAuthor }),
@@ -458,11 +492,13 @@ export class SqlStore {
458
492
  };
459
493
  }
460
494
  // --- sessions ---
461
- async listSessions() {
462
- return this.sql
463
- .exec("SELECT * FROM sessions ORDER BY lastActiveAt DESC")
464
- .toArray()
465
- .map((r) => this.rowToSession(r));
495
+ async listSessions(project) {
496
+ const rows = project === undefined
497
+ ? this.sql.exec("SELECT * FROM sessions ORDER BY lastActiveAt DESC").toArray()
498
+ : this.sql
499
+ .exec("SELECT * FROM sessions WHERE project = ? ORDER BY lastActiveAt DESC", project)
500
+ .toArray();
501
+ return rows.map((r) => this.rowToSession(r));
466
502
  }
467
503
  async getSession(id) {
468
504
  const rows = this.sql.exec("SELECT * FROM sessions WHERE id = ?", id).toArray();
@@ -485,8 +521,10 @@ export class SqlStore {
485
521
  lastActiveAt: now,
486
522
  agentSeq: 0,
487
523
  project: stripNul(input.project)?.trim() || projectFromCwd(stripNul(input.cwd ?? null)) || null,
524
+ agentActiveAt: now,
525
+ keyed: !!stripNul(input.key)?.trim(),
488
526
  };
489
- this.sql.exec("INSERT INTO sessions (id, agent, title, cwd, createdAt, lastActiveAt, agentSeq, project, key) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)", session.id, session.agent, session.title, session.cwd, session.createdAt, session.lastActiveAt, session.project, stripNul(input.key)?.trim() || null);
527
+ this.sql.exec("INSERT INTO sessions (id, agent, title, cwd, createdAt, lastActiveAt, agentSeq, project, key, agentActiveAt) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)", session.id, session.agent, session.title, session.cwd, session.createdAt, session.lastActiveAt, session.project, stripNul(input.key)?.trim() || null, session.agentActiveAt);
490
528
  return session;
491
529
  }
492
530
  async renameSession(id, title) {
@@ -528,6 +566,14 @@ export class SqlStore {
528
566
  async markAgentSeen(sessionId, seq) {
529
567
  this.sql.exec("UPDATE sessions SET agentSeq = ? WHERE id = ? AND agentSeq < ?", seq, sessionId, seq);
530
568
  }
569
+ async touchAgent(sessionId) {
570
+ this.sql.exec("UPDATE sessions SET agentActiveAt = ? WHERE id = ?", new Date().toISOString(), sessionId);
571
+ }
572
+ async handOff(sessionId, seen, toSeq) {
573
+ return (this.sql
574
+ .exec("UPDATE sessions SET agentSeq = ? WHERE id = ? AND agentSeq = ? AND agentActiveAt IS ? AND agentSeq < ? RETURNING id", toSeq, sessionId, seen.agentSeq, seen.agentActiveAt, toSeq)
575
+ .toArray().length > 0);
576
+ }
531
577
  // --- settings ---
532
578
  async getSetting(key) {
533
579
  return this.settingSync(key);
@@ -570,12 +616,14 @@ export class SqlStore {
570
616
  states: (input.states ?? []).map((s) => stripNul(s)),
571
617
  asks: [],
572
618
  knobs: input.knobs ?? {},
619
+ widths: input.widths ?? [],
573
620
  draft: null,
574
621
  sessionId: input.sessionId ?? null,
622
+ resolvedAt: null,
575
623
  createdAt: now,
576
624
  updatedAt: now,
577
625
  };
578
- this.sql.exec("INSERT INTO mocks (id, project, slug, title, kind, states, asks, knobs, draft, sessionId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, '[]', ?, NULL, ?, ?, ?)", mock.id, mock.project, mock.slug, mock.title, mock.kind, JSON.stringify(mock.states), JSON.stringify(mock.knobs), mock.sessionId, mock.createdAt, mock.updatedAt);
626
+ this.sql.exec("INSERT INTO mocks (id, project, slug, title, kind, states, asks, knobs, widths, draft, sessionId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, '[]', ?, ?, NULL, ?, ?, ?)", mock.id, mock.project, mock.slug, mock.title, mock.kind, JSON.stringify(mock.states), JSON.stringify(mock.knobs), JSON.stringify(mock.widths), mock.sessionId, mock.createdAt, mock.updatedAt);
579
627
  return mock;
580
628
  }
581
629
  async updateMock(id, patch) {
@@ -589,29 +637,86 @@ export class SqlStore {
589
637
  ...(patch.states !== undefined && { states: patch.states.map((s) => stripNul(s)) }),
590
638
  ...(patch.asks !== undefined && { asks: patch.asks }),
591
639
  ...(patch.knobs !== undefined && { knobs: patch.knobs }),
640
+ ...(patch.widths !== undefined && { widths: patch.widths }),
592
641
  ...(patch.sessionId !== undefined && { sessionId: patch.sessionId }),
642
+ ...(patch.resolvedAt !== undefined && { resolvedAt: patch.resolvedAt }),
593
643
  updatedAt: new Date().toISOString(),
594
644
  };
595
- this.sql.exec("UPDATE mocks SET title = ?, kind = ?, states = ?, asks = ?, knobs = ?, sessionId = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, JSON.stringify(next.states), JSON.stringify(next.asks), JSON.stringify(next.knobs), next.sessionId, next.updatedAt, id);
645
+ this.sql.exec("UPDATE mocks SET title = ?, kind = ?, states = ?, asks = ?, knobs = ?, widths = ?, sessionId = ?, resolvedAt = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, JSON.stringify(next.states), JSON.stringify(next.asks), JSON.stringify(next.knobs), JSON.stringify(next.widths), next.sessionId, next.resolvedAt, next.updatedAt, id);
646
+ if (patch.asks !== undefined)
647
+ this.invalidateAssetRefs();
596
648
  return next;
597
649
  }
598
650
  async removeMock(id) {
599
- if (!(await this.getMock(id)))
651
+ const mock = await this.getMock(id);
652
+ if (!mock)
600
653
  return false;
601
654
  this.atomic(() => {
602
655
  this.sql.exec("DELETE FROM comments WHERE mockId = ?", id);
603
656
  this.sql.exec("DELETE FROM post_versions WHERE postId IN (SELECT id FROM posts WHERE mockId = ?)", id);
604
657
  this.sql.exec("DELETE FROM posts WHERE mockId = ?", id);
605
658
  this.sql.exec("DELETE FROM mocks WHERE id = ?", id);
659
+ for (const flow of this.flowsOf(mock.project)) {
660
+ const steps = flow.steps.filter((x) => x.mock !== mock.slug);
661
+ if (steps.length !== flow.steps.length)
662
+ this.writeFlow(flow, steps);
663
+ }
606
664
  });
607
665
  this.invalidateAssetRefs();
608
666
  return true;
609
667
  }
668
+ // --- flows ---
669
+ flowsOf(project) {
670
+ return this.sql
671
+ .exec("SELECT * FROM flows WHERE project = ? ORDER BY createdAt, rowid", project)
672
+ .toArray()
673
+ .map((r) => ({
674
+ project: r.project,
675
+ name: r.name,
676
+ steps: parseJson(r.steps, []),
677
+ createdAt: r.createdAt,
678
+ updatedAt: r.updatedAt,
679
+ }));
680
+ }
681
+ // An emptied flow is deleted rather than kept as a name with nothing to walk.
682
+ writeFlow(flow, steps) {
683
+ if (steps.length === 0) {
684
+ this.sql.exec("DELETE FROM flows WHERE project = ? AND name = ?", flow.project, flow.name);
685
+ return null;
686
+ }
687
+ const next = { ...flow, steps, updatedAt: new Date().toISOString() };
688
+ this.sql.exec("INSERT INTO flows (project, name, steps, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?) ON CONFLICT(project, name) DO UPDATE SET steps = excluded.steps, updatedAt = excluded.updatedAt", next.project, next.name, JSON.stringify(next.steps), next.createdAt, next.updatedAt);
689
+ return next;
690
+ }
691
+ async listFlows(project) {
692
+ return this.flowsOf(project);
693
+ }
694
+ async placeFlowStep(project, name, screen, step) {
695
+ const clean = { mock: stripNul(screen.mock), state: stripNul(screen.state) };
696
+ const label = stripNul(name);
697
+ return this.atomic(() => {
698
+ const now = new Date().toISOString();
699
+ const flow = this.flowsOf(project).find((f) => f.name === label) ?? {
700
+ project,
701
+ name: label,
702
+ steps: [],
703
+ createdAt: now,
704
+ updatedAt: now,
705
+ };
706
+ const steps = flow.steps.filter((x) => !sameScreen(x, clean));
707
+ if (step > 0)
708
+ steps.splice(Math.min(step, steps.length + 1) - 1, 0, clean);
709
+ if (steps.length === 0 && flow.steps.length === 0)
710
+ return null;
711
+ return this.writeFlow(flow, steps);
712
+ });
713
+ }
610
714
  async putDraft(mockId, draft) {
611
715
  const mock = await this.getMock(mockId);
612
716
  if (!mock)
613
717
  return null;
614
718
  this.sql.exec("UPDATE mocks SET draft = ? WHERE id = ?", draft ? JSON.stringify(draft) : null, mockId);
719
+ this.invalidateAssetRefs();
615
720
  return { ...mock, draft };
616
721
  }
617
722
  touchMock(mockId, at) {
@@ -674,12 +779,14 @@ export class SqlStore {
674
779
  version: 1,
675
780
  history: [],
676
781
  ...(input.knobs ? { knobs: input.knobs } : {}),
677
- slots: input.slots ?? [],
782
+ ...(stripNul(input.note ?? "").trim() ? { note: stripNul(input.note ?? "").trim() } : {}),
783
+ embeds: input.embeds ?? [],
784
+ ...(input.forkedFrom ? { forkedFrom: input.forkedFrom } : {}),
678
785
  ...(input.from === undefined ? {} : { from: input.from }),
679
786
  ...(input.prompt === undefined ? {} : { prompt: stripNul(input.prompt) }),
680
787
  ...(input.author === undefined ? {} : { author: stripNul(input.author) }),
681
788
  };
682
- this.sql.exec("INSERT INTO posts (id, sessionId, mockId, state, variant, status, title, surfaces, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)", post.id, post.sessionId, post.mock, post.state, post.variant, post.title, JSON.stringify(post.surfaces), post.knobs ? JSON.stringify(post.knobs) : null, JSON.stringify(post.slots), post.createdAt, post.updatedAt, post.from ?? null, post.prompt ?? null, post.author ?? null);
789
+ this.sql.exec("INSERT INTO posts (id, sessionId, mockId, state, variant, status, title, surfaces, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor, note, forkedFrom) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)", post.id, post.sessionId, post.mock, post.state, post.variant, post.title, JSON.stringify(post.surfaces), post.knobs ? JSON.stringify(post.knobs) : null, JSON.stringify(post.embeds), post.createdAt, post.updatedAt, post.from ?? null, post.prompt ?? null, post.author ?? null, post.note ?? null, post.forkedFrom ? JSON.stringify(post.forkedFrom) : null);
683
790
  this.touch(input.sessionId);
684
791
  this.touchMock(post.mock, now);
685
792
  this.addAssetRefs(post.surfaces);
@@ -701,13 +808,17 @@ export class SqlStore {
701
808
  ...(post.prompt === undefined ? {} : { prompt: post.prompt }),
702
809
  ...(post.author === undefined ? {} : { author: post.author }),
703
810
  };
811
+ // The version in use is kept past the limit: export and embeds resolve to it.
812
+ const pin = post.acceptedVersion ?? -1;
704
813
  const history = [...post.history, archived];
705
- if (history.length > HISTORY_LIMIT)
706
- history.shift();
814
+ const prunable = history.filter((h) => h.version !== pin);
815
+ if (prunable.length > HISTORY_LIMIT)
816
+ history.splice(history.indexOf(prunable[0]), 1);
707
817
  const title = patch.title !== undefined ? stripNul(patch.title).trim() || post.title : post.title;
708
818
  const surfaces = patch.surfaces !== undefined ? normalizeSurfaceIds(patch.surfaces) : post.surfaces;
709
819
  const knobs = patch.knobs === undefined ? post.knobs : (patch.knobs ?? undefined);
710
- const slots = patch.slots ?? post.slots;
820
+ const note = patch.note === undefined ? post.note : stripNul(patch.note).trim() || undefined;
821
+ const embeds = patch.embeds ?? post.embeds;
711
822
  const state = patch.state === undefined ? post.state : patch.state;
712
823
  const version = post.version + 1;
713
824
  const updatedAt = new Date().toISOString();
@@ -715,22 +826,23 @@ export class SqlStore {
715
826
  const from = patch.from ?? post.version;
716
827
  const prompt = stripNul(patch.prompt ?? "");
717
828
  const author = patch.author === undefined ? null : stripNul(patch.author);
718
- this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, knobs = ?, slots = ?, state = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), knobs ? JSON.stringify(knobs) : null, JSON.stringify(slots), state, updatedAt, version, from, prompt, author, id, post.version);
829
+ this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, knobs = ?, note = ?, slots = ?, state = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), knobs ? JSON.stringify(knobs) : null, note ?? null, JSON.stringify(embeds), state, updatedAt, version, from, prompt, author, id, post.version);
719
830
  const affected = this.sql.exec("SELECT changes() AS n").one().n;
720
831
  if (affected > 0) {
721
832
  this.insertVersion(id, archived);
722
- this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version <= ?", id, version - 1 - HISTORY_LIMIT);
833
+ this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version <= ? AND version != ?", id, version - 1 - HISTORY_LIMIT, pin);
723
834
  this.touch(post.sessionId);
724
835
  this.touchMock(post.mock, updatedAt);
725
836
  if (patch.surfaces !== undefined)
726
837
  this.addAssetRefs(surfaces);
727
- const { knobs: _knobs, author: _author, ...rest } = post;
838
+ const { knobs: _knobs, note: _note, author: _author, ...rest } = post;
728
839
  return {
729
840
  ...rest,
730
841
  title,
731
842
  surfaces,
732
843
  ...(knobs ? { knobs } : {}),
733
- slots,
844
+ ...(note ? { note } : {}),
845
+ embeds,
734
846
  state,
735
847
  version,
736
848
  updatedAt,
@@ -754,12 +866,36 @@ export class SqlStore {
754
866
  this.invalidateAssetRefs();
755
867
  return true;
756
868
  }
757
- async setPostStatus(id, status) {
869
+ async setPostStatus(id, status, acceptedVersion) {
870
+ const post = await this.getPost(id);
871
+ if (!post)
872
+ return null;
873
+ const pin = status === "accepted" ? (acceptedVersion ?? post.version) : null;
874
+ this.sql.exec("UPDATE posts SET status = ?, acceptedVersion = ? WHERE id = ?", status, pin, id);
875
+ const { acceptedVersion: _, ...rest } = post;
876
+ return pin === null ? { ...rest, status } : { ...rest, status, acceptedVersion: pin };
877
+ }
878
+ async setPostStarred(id, starred) {
758
879
  const post = await this.getPost(id);
759
880
  if (!post)
760
881
  return null;
761
- this.sql.exec("UPDATE posts SET status = ? WHERE id = ?", status, id);
762
- return { ...post, status };
882
+ this.sql.exec("UPDATE posts SET starred = ? WHERE id = ?", starred ? 1 : 0, id);
883
+ const { starred: _, ...rest } = post;
884
+ return starred ? { ...rest, starred: true } : rest;
885
+ }
886
+ async setVersionHidden(id, version, hidden) {
887
+ const post = await this.getPost(id);
888
+ if (!post)
889
+ return null;
890
+ const set = new Set(post.hiddenVersions ?? []);
891
+ if (hidden)
892
+ set.add(version);
893
+ else
894
+ set.delete(version);
895
+ const list = [...set].sort((a, b) => a - b);
896
+ this.sql.exec("UPDATE posts SET hiddenVersions = ? WHERE id = ?", list.length ? JSON.stringify(list) : null, id);
897
+ const { hiddenVersions: _, ...rest } = post;
898
+ return list.length ? { ...rest, hiddenVersions: list } : rest;
763
899
  }
764
900
  // --- comments ---
765
901
  async listComments(query) {
@@ -826,6 +962,8 @@ export class SqlStore {
826
962
  return null;
827
963
  const comment = this.rowToComment(rows[0]);
828
964
  this.sql.exec("DELETE FROM comments WHERE id = ?", id);
965
+ if (comment.kind === "reply")
966
+ this.invalidateAssetRefs();
829
967
  this.touch(comment.sessionId);
830
968
  return comment;
831
969
  }
@@ -844,13 +982,22 @@ export class SqlStore {
844
982
  postVersion: input.payload.version,
845
983
  payload: input.payload,
846
984
  });
847
- this.sql.exec("UPDATE mocks SET asks = ?, draft = NULL WHERE id = ?", JSON.stringify(input.asks), input.mockId);
848
- for (const id of input.accept) {
849
- this.sql.exec("UPDATE posts SET status = 'accepted' WHERE id = ? AND mockId = ?", id, input.mockId);
985
+ this.sql.exec(input.keepDraft
986
+ ? "UPDATE mocks SET asks = ? WHERE id = ?"
987
+ : "UPDATE mocks SET asks = ?, draft = NULL WHERE id = ?", JSON.stringify(input.asks), input.mockId);
988
+ if (input.resolvedAt !== undefined) {
989
+ this.sql.exec("UPDATE mocks SET resolvedAt = ?, updatedAt = ? WHERE id = ?", input.resolvedAt, input.resolvedAt, input.mockId);
990
+ }
991
+ for (const { id, version } of input.accept) {
992
+ this.sql.exec("UPDATE posts SET status = 'accepted', acceptedVersion = ? WHERE id = ? AND mockId = ?", version, id, input.mockId);
850
993
  }
851
994
  for (const id of input.archive) {
852
- this.sql.exec("UPDATE posts SET status = 'archived' WHERE id = ? AND mockId = ?", id, input.mockId);
995
+ this.sql.exec("UPDATE posts SET status = 'archived', acceptedVersion = NULL WHERE id = ? AND mockId = ?", id, input.mockId);
853
996
  }
997
+ for (const id of input.drop ?? []) {
998
+ this.sql.exec("UPDATE posts SET status = 'dropped', acceptedVersion = NULL WHERE id = ? AND mockId = ?", id, input.mockId);
999
+ }
1000
+ this.invalidateAssetRefs();
854
1001
  return comment;
855
1002
  });
856
1003
  }
@@ -865,6 +1012,17 @@ export class SqlStore {
865
1012
  for (const r of this.sql.exec("SELECT surfaces FROM post_versions").toArray()) {
866
1013
  collectAssetIds(parseJson(r.surfaces, []), out);
867
1014
  }
1015
+ // The user's attachments: in a draft, in a sent reply, on an answered ask.
1016
+ for (const r of this.sql.exec("SELECT asks, draft FROM mocks").toArray()) {
1017
+ collectAttachedIds(parseJson(r.draft, null), out);
1018
+ for (const ask of parseJson(r.asks, []))
1019
+ collectAttachedIds(ask, out);
1020
+ }
1021
+ for (const r of this.sql
1022
+ .exec("SELECT payload FROM comments WHERE kind = 'reply' AND payload IS NOT NULL")
1023
+ .toArray()) {
1024
+ collectAttachedIds(parseJson(r.payload, null), out);
1025
+ }
868
1026
  // An installed icon set is referenced by its project's design, not by a
869
1027
  // surface; evicting it would blank that project's icons.
870
1028
  for (const r of this.sql