scenri 0.10.3 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.0](https://github.com/tonygorb/Scenri/compare/v0.10.3...v0.11.0) (2026-09-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * first use, a tutor that walks the real task, with First steps and Learn ([47e9775](https://github.com/tonygorb/Scenri/commit/47e977569049803f0544ce19ad0289cdc8e187f5))
9
+ * Learn, every lesson in one place, begun and continued with the same tutor ([1d378e1](https://github.com/tonygorb/Scenri/commit/1d378e17f22a8ebf64878d67caa0996027e5f1f6))
10
+ * **studio:** New lands on Create with the add panel open ([674bf9c](https://github.com/tonygorb/Scenri/commit/674bf9cbb369443e75b78586210a0b0b12c94826))
11
+ * **studio:** New's plus rides a gold disc ([673a17b](https://github.com/tonygorb/Scenri/commit/673a17b1e48eb491ecc2c9a1d122955d11e24d0d))
12
+ * **studio:** the brand menu leads with where you have been and stays one height ([e28a857](https://github.com/tonygorb/Scenri/commit/e28a8573f797f75815687239cd58c1b4d01518e7))
13
+ * **studio:** the new top bar ([e1d262b](https://github.com/tonygorb/Scenri/commit/e1d262b9eb21a4fd788dae8acd487096223b87c6))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * a product made from the picker is named, and Enter on a control is that control's ([7383c7b](https://github.com/tonygorb/Scenri/commit/7383c7baa6fde61dcbf060c68cb429960caee73a))
19
+ * a task ended on a surface is not begun there again, and an empty chip panel foot is not drawn ([2b450fd](https://github.com/tonygorb/Scenri/commit/2b450fd3b7aeb875fee5649b670f3839af0f7894))
20
+ * closing Learn gives the keyboard back to Help when what opened it is gone ([c12d690](https://github.com/tonygorb/Scenri/commit/c12d690fad3e7ce4d1cfaed05aa6982a97d5de4c))
21
+ * in the walk a chip comes in through its ask and leaves only through Back ([a0e9fea](https://github.com/tonygorb/Scenri/commit/a0e9fea7426755ba020f766f0a0678df96542e2c))
22
+ * **studio:** a bar menu answers the next click, even while it is closing ([7b0af47](https://github.com/tonygorb/Scenri/commit/7b0af476d0af0691be60616687955c3521171787))
23
+ * **studio:** a brand icon that is its own ground fills its circle ([0f356fb](https://github.com/tonygorb/Scenri/commit/0f356fbbd19e899ae41751851f873fedbce4d9b5))
24
+ * **studio:** a brand's circle shows its icon, or its initial, never a shrunken wordmark ([6c8415d](https://github.com/tonygorb/Scenri/commit/6c8415da61b261629cee734366a1cece630a58c4))
25
+ * **studio:** New pressed with the add panel open leaves it open and focuses the brief ([37050a4](https://github.com/tonygorb/Scenri/commit/37050a4e4b99f431649f97423ab6596f8ba52dad))
26
+ * **studio:** New wears round ends, the mark shows its box, and Clear all joins its list ([2ed412e](https://github.com/tonygorb/Scenri/commit/2ed412e1faf4d60378b0996f02373ea804b27fe9))
27
+ * **studio:** New's plus sits on an ink disc, and the bar no longer shrinks ([86ceaa3](https://github.com/tonygorb/Scenri/commit/86ceaa33dc16856a1c3fa2e4950360d1a77c5eec))
28
+ * **studio:** pointing at New's plus lights the whole button ([d3a4973](https://github.com/tonygorb/Scenri/commit/d3a4973dc8ee9a0ce090e24fd3f3f8a591c1b0f5))
29
+ * **studio:** the bar wears the full Scenri lockup, bare ([25efd02](https://github.com/tonygorb/Scenri/commit/25efd0292c1d0982ec1f027364489306cf294bea))
30
+ * **studio:** the brand list ends on a whole row ([a20da23](https://github.com/tonygorb/Scenri/commit/a20da23fd89240fd533944ad222c62144795a12e))
31
+ * **studio:** the brand list fades into the line under it while there is more ([18073f8](https://github.com/tonygorb/Scenri/commit/18073f8ea67157c64bbb7b676eb9aade51276614))
32
+ * **studio:** the brand list is ten rows tall ([df5b4e1](https://github.com/tonygorb/Scenri/commit/df5b4e1b834aabf9b4dc22338e83ce99a19550e3))
33
+ * **studio:** the brand list runs straight into the line under it ([402e017](https://github.com/tonygorb/Scenri/commit/402e0176e3bb7ae5cc799af77de4852e92608ba1))
34
+ * **studio:** the brand menu is one compact list ([1e3dc70](https://github.com/tonygorb/Scenri/commit/1e3dc7002cae58fbc8bacc567cf3a924a38a6a1c))
35
+ * **studio:** the brand menu is one list, and the help button leaves this branch ([2e717d1](https://github.com/tonygorb/Scenri/commit/2e717d1474f45e625e85bef36b69d3397cb20925))
36
+ * the tutor keeps what a moment is about in sight, and lets people go their own way ([41e667e](https://github.com/tonygorb/Scenri/commit/41e667e94a930d85bf1ad88fbba4bf00ae6f5ece))
37
+ * the welcome reads top to bottom, speaks plainly, and opens again from Help ([7edb94f](https://github.com/tonygorb/Scenri/commit/7edb94f289e17c1c4255cc6ef683758bc21aea17))
38
+
3
39
  ## [0.10.3](https://github.com/tonygorb/Scenri/compare/v0.10.2...v0.10.3) (2026-09-18)
4
40
 
5
41
 
@@ -1344,6 +1344,55 @@ function createStore(db) {
1344
1344
  setOverlays(id, overlays) {
1345
1345
  db.prepare("UPDATE nodes SET overlays=? WHERE id=?").run(JSON.stringify(overlays), id);
1346
1346
  },
1347
+ // first use: what the library already proves, read by the guide record
1348
+ /** The database's own clock, in the format every node's created_at uses. */
1349
+ now() {
1350
+ return db.prepare("SELECT strftime('%Y-%m-%d %H:%M:%f','now') AS t").get().t;
1351
+ },
1352
+ /** Whether any brand holds a finished shot, or a finished refinement, with a picture. */
1353
+ hasFinishedNode(kind) {
1354
+ return !!db.prepare("SELECT 1 FROM nodes WHERE kind=? AND status='done' AND images != '[]' LIMIT 1").get(kind);
1355
+ },
1356
+ /** Whether any brand has imported a product from a store. */
1357
+ hasCatalogProduct() {
1358
+ return !!db.prepare("SELECT 1 FROM catalog_products LIMIT 1").get();
1359
+ },
1360
+ /** A brand's shots of one kind made at or after a moment, newest first: what a guided task has made. */
1361
+ nodesSince(brandId, kind, since, limit = 8) {
1362
+ const rows = db.prepare(
1363
+ `SELECT id, kind, status, images, created_at FROM nodes
1364
+ WHERE project_id IN (SELECT id FROM projects WHERE brand_id = ?)
1365
+ AND kind = ? AND created_at >= ?
1366
+ ORDER BY created_at DESC, id DESC LIMIT ?`
1367
+ ).all(brandId, kind, since, limit);
1368
+ return rows.map((r) => ({
1369
+ id: r.id,
1370
+ kind: r.kind,
1371
+ status: r.status,
1372
+ images: (() => {
1373
+ try {
1374
+ const a = JSON.parse(r.images);
1375
+ return Array.isArray(a) ? a.length : 0;
1376
+ } catch {
1377
+ return 0;
1378
+ }
1379
+ })(),
1380
+ createdAt: r.created_at
1381
+ }));
1382
+ },
1383
+ /** The newest presenter draft a brand started at or after a moment, never an edit session's. */
1384
+ presenterDraftSince(brandId, since) {
1385
+ const rows = db.prepare(
1386
+ "SELECT id, json FROM presenter_drafts WHERE brand_id = ? AND created_at >= ? ORDER BY created_at DESC, id DESC"
1387
+ ).all(brandId, since);
1388
+ for (const r of rows) {
1389
+ try {
1390
+ if (!JSON.parse(r.json)?.presenterId) return r.id;
1391
+ } catch {
1392
+ }
1393
+ }
1394
+ return null;
1395
+ },
1347
1396
  // settings
1348
1397
  getSetting(key) {
1349
1398
  const r = db.prepare("SELECT value FROM settings WHERE key=?").get(key);
@@ -2175,5 +2224,5 @@ function createCore(homeDir = defaultHome()) {
2175
2224
  }
2176
2225
 
2177
2226
  export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug };
2178
- //# sourceMappingURL=chunk-UUYMUOZQ.js.map
2179
- //# sourceMappingURL=chunk-UUYMUOZQ.js.map
2227
+ //# sourceMappingURL=chunk-K6IELFL3.js.map
2228
+ //# sourceMappingURL=chunk-K6IELFL3.js.map
@@ -463,7 +463,7 @@ async function addOrRepair(ownEntry, say, over) {
463
463
  }
464
464
  async function rememberDecline(home) {
465
465
  try {
466
- const { createCore } = await import('./src-QZZDZM7I.js');
466
+ const { createCore } = await import('./src-TC4UQ7H6.js');
467
467
  const core = createCore(home);
468
468
  try {
469
469
  core.store.setSetting("desktop.prompt", "declined");
@@ -530,5 +530,5 @@ function tail(path, bytes = 4096) {
530
530
  }
531
531
 
532
532
  export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand };
533
- //# sourceMappingURL=cli-3KKS7BE4.js.map
534
- //# sourceMappingURL=cli-3KKS7BE4.js.map
533
+ //# sourceMappingURL=cli-GMRPOISO.js.map
534
+ //# sourceMappingURL=cli-GMRPOISO.js.map
package/dist/index.js CHANGED
@@ -86,12 +86,12 @@ try {
86
86
  break;
87
87
  }
88
88
  case "desktop": {
89
- const { runDesktopCommand } = await import('./cli-3KKS7BE4.js');
89
+ const { runDesktopCommand } = await import('./cli-GMRPOISO.js');
90
90
  process.exit(await runDesktopCommand(command, fileURLToPath(import.meta.url)));
91
91
  break;
92
92
  }
93
93
  case "open": {
94
- const { runOpenCommand } = await import('./cli-3KKS7BE4.js');
94
+ const { runOpenCommand } = await import('./cli-GMRPOISO.js');
95
95
  process.exit(await runOpenCommand(fileURLToPath(import.meta.url)));
96
96
  break;
97
97
  }
package/dist/serve.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createUpdateChecker, classify, findNpm, isReleaseTriplet, stageVersion } from './chunk-YK37H6DM.js';
2
2
  import { shouldAdoptRunning, anotherScenriLines, portBusyLines } from './chunk-CMLJGPYC.js';
3
- import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, termMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-UUYMUOZQ.js';
3
+ import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, termMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-K6IELFL3.js';
4
4
  import { detectInstallKind } from './chunk-PAIAXRAC.js';
5
5
  export { detectInstallKind } from './chunk-PAIAXRAC.js';
6
6
  import { readMeta, repoSlug } from './chunk-Y3ZPBPLP.js';
@@ -1287,12 +1287,12 @@ function createCodexAnalyzer(opts = {}) {
1287
1287
  };
1288
1288
  }
1289
1289
  function buildPrompt(req, refCount, problems) {
1290
- const evidence = refCount === 0 ? "You have no reference images. Work from the description alone." : refCount === 1 ? "One reference image is attached." : `${refCount} reference images are attached.`;
1290
+ const evidence2 = refCount === 0 ? "You have no reference images. Work from the description alone." : refCount === 1 ? "One reference image is attached." : `${refCount} reference images are attached.`;
1291
1291
  const body = req.kind === "presenter" ? presenterBody(req, refCount) : sceneBody(req);
1292
1292
  const revision = req.priorDraft ? ` You are revising an existing record, not starting over: keep everything that is not being corrected. Current record: ${JSON.stringify(req.priorDraft)}.` : "";
1293
1293
  const correction = req.correction ? ` The correction to apply: ${req.correction}.` : "";
1294
1294
  const retry = problems.length ? ` Your last answer was rejected: ${problems.join(" ")} Fix exactly that and write the file again.` : "";
1295
- return `${evidence}${revision}${correction} ${body} Write strict JSON, and nothing but JSON, to a file called ${OUT_FILE} in the current directory (you may run the commands needed to write it). Do not browse the web or explore files. No prose, no markdown fences, no commentary.${retry}`;
1295
+ return `${evidence2}${revision}${correction} ${body} Write strict JSON, and nothing but JSON, to a file called ${OUT_FILE} in the current directory (you may run the commands needed to write it). Do not browse the web or explore files. No prose, no markdown fences, no commentary.${retry}`;
1296
1296
  }
1297
1297
  function presenterBody(req, refCount) {
1298
1298
  const same = refCount > 1 ? " Every attached photograph is the same person." : "";
@@ -3248,7 +3248,7 @@ function compileBrief(brief, ctx) {
3248
3248
  );
3249
3249
  if (names.length) {
3250
3250
  warnings.push(
3251
- `${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out \u2014 ${ctx.engineCaps.displayName} reads no reference images.`
3251
+ `${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out: ${ctx.engineCaps.displayName} reads no reference images.`
3252
3252
  );
3253
3253
  }
3254
3254
  }
@@ -5207,18 +5207,18 @@ function mergeScrape(existing, scraped) {
5207
5207
  async function inspectMark(buf, toPng2) {
5208
5208
  const png = await toPng2(buf);
5209
5209
  const meta = await sharp21(png).metadata();
5210
- let blank = false;
5210
+ let blank2 = false;
5211
5211
  try {
5212
5212
  const flat = await sharp21(png).flatten({ background: "#ffffff" }).toBuffer();
5213
5213
  const stats = await sharp21(flat).stats();
5214
- blank = stats.channels.slice(0, 3).every((c) => c.min > 200);
5214
+ blank2 = stats.channels.slice(0, 3).every((c) => c.min > 200);
5215
5215
  } catch {
5216
5216
  }
5217
5217
  return {
5218
5218
  longEdge: Math.max(meta.width ?? 0, meta.height ?? 0) || null,
5219
5219
  width: meta.width ?? null,
5220
5220
  height: meta.height ?? null,
5221
- blank
5221
+ blank: blank2
5222
5222
  };
5223
5223
  }
5224
5224
 
@@ -11287,7 +11287,7 @@ function defaultShowcaseDir() {
11287
11287
 
11288
11288
  // src/routes/showcase.ts
11289
11289
  function registerShowcaseRoutes(app, deps) {
11290
- const { templatesRoot } = deps;
11290
+ const { templatesRoot, thumbs } = deps;
11291
11291
  const { showcase } = loadShowcase(join(templatesRoot, "showcase"));
11292
11292
  const showcaseHeroPath = (id) => contentFile(templatesRoot, "previews", "showcase", `${id}.jpg`);
11293
11293
  const decorateShowcase = (s) => ({
@@ -11301,7 +11301,8 @@ function registerShowcaseRoutes(app, deps) {
11301
11301
  app.get("/api/showcase-previews/:file", async (req, reply) => {
11302
11302
  const m = /^([a-z0-9-]+)\.jpg$/.exec(String(req.params.file));
11303
11303
  if (!m || !existsSync(showcaseHeroPath(m[1]))) return reply.status(404).send({ error: "no preview" });
11304
- return serveJpeg(req, reply, showcaseHeroPath(m[1]));
11304
+ const path = showcaseHeroPath(m[1]);
11305
+ return serveJpegSized(req, reply, path, thumbs, fileKey("showcase", m[1], path));
11305
11306
  });
11306
11307
  }
11307
11308
 
@@ -11703,6 +11704,29 @@ function registerImageRoutes(app, deps) {
11703
11704
 
11704
11705
  // src/release/notes.data.ts
11705
11706
  var RELEASES = [
11707
+ {
11708
+ version: "0.11.0",
11709
+ date: "2026-09-18",
11710
+ title: "A new top bar, and a tutor that makes your first shot with you.",
11711
+ sections: [
11712
+ {
11713
+ heading: "Top bar",
11714
+ body: "The five places sit in the middle of the bar, with the one you are on underlined. New opens Create with the add panel open, and its arrow adds a product, presenter or scene instead. Activity replaces the bell: while a shot renders it shows that picture inside a filling ring, and its panel lists what is in progress and your notifications together, with Mark all read and Clear all. On a phone every panel in the bar opens as a sheet."
11715
+ },
11716
+ {
11717
+ heading: "Brand",
11718
+ body: "The brand menu is one short list, the brand you are in first and the rest A to Z, with a search once it grows long, so Settings and Shut down stay in reach. A brand's circle shows its icon, or its initial, never a shrunken wordmark."
11719
+ },
11720
+ {
11721
+ heading: "Learn",
11722
+ body: "A new install is welcomed and makes its first shot with a tutor, one step at a time: a product, a presenter, a scene, then the words. First steps on Home lists four lessons to start with and leaves once they are done, and Learn holds all five, from adding your product to refining a shot. Help sits in the corner of the page from 1024 pixels wide and in the bar below that, with Learn, the welcome, keyboard shortcuts and What's new."
11723
+ },
11724
+ {
11725
+ heading: "Fixes",
11726
+ body: "A product made from the picker goes into the brief by its name instead of as a missing product, and Enter on a focused control in Create presses that control rather than opening the selected shot."
11727
+ }
11728
+ ]
11729
+ },
11706
11730
  {
11707
11731
  version: "0.10.3",
11708
11732
  date: "2026-09-18",
@@ -12765,6 +12789,165 @@ function registerSystemRoutes(app, deps) {
12765
12789
  });
12766
12790
  }
12767
12791
 
12792
+ // src/routes/guide.ts
12793
+ var TASKS = ["first-shot", "refine", "product", "presenter", "scene"];
12794
+ var MILESTONES = ["shot", "refine", "product", "presenter", "scene"];
12795
+ var KEY = "guide";
12796
+ var isTask = (t) => TASKS.includes(t);
12797
+ var isMilestone = (m) => MILESTONES.includes(m);
12798
+ var num2 = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : 0;
12799
+ function blank(eligible) {
12800
+ return { v: 2, eligible, welcome: null, hidden: null, done: {}, dismissed: [], active: null };
12801
+ }
12802
+ function parse2(raw) {
12803
+ if (!raw) return null;
12804
+ try {
12805
+ const j = JSON.parse(raw);
12806
+ if (typeof j.eligible !== "boolean") return null;
12807
+ if (j.v !== 2) return null;
12808
+ const done = {};
12809
+ if (j.done && typeof j.done === "object") {
12810
+ for (const [k, at] of Object.entries(j.done)) if (isMilestone(k) && typeof at === "string") done[k] = at;
12811
+ }
12812
+ const a = j.active;
12813
+ const b = a?.baseline;
12814
+ const active = a && isTask(a.task) && typeof a.brandId === "string" && typeof a.since === "string" ? {
12815
+ task: a.task,
12816
+ brandId: a.brandId,
12817
+ since: a.since,
12818
+ baseline: { products: num2(b?.products), presenters: num2(b?.presenters), scenes: num2(b?.scenes) },
12819
+ ...a.paused === true ? { paused: true } : {}
12820
+ } : null;
12821
+ return {
12822
+ v: 2,
12823
+ eligible: j.eligible,
12824
+ welcome: j.welcome === "taken" || j.welcome === "declined" ? j.welcome : null,
12825
+ hidden: typeof j.hidden === "boolean" ? j.hidden : null,
12826
+ done,
12827
+ dismissed: Array.isArray(j.dismissed) ? j.dismissed.filter(isTask) : [],
12828
+ active
12829
+ };
12830
+ } catch {
12831
+ return null;
12832
+ }
12833
+ }
12834
+ function write(store, r) {
12835
+ store.setSetting(KEY, JSON.stringify(r));
12836
+ }
12837
+ function stampGuide(store, version) {
12838
+ if (store.getSetting(KEY) !== null) return;
12839
+ const eligible = store.listBrands().length === 0;
12840
+ if (eligible) {
12841
+ if (!store.getSetting("install.firstVersion")) store.setSetting("install.firstVersion", version);
12842
+ const seen = store.getSetting("whatsnew.seen");
12843
+ if (!seen || compareSemver(seen, version) < 0) store.setSetting("whatsnew.seen", version);
12844
+ }
12845
+ write(store, blank(eligible));
12846
+ }
12847
+ function countsOf(core, brandId) {
12848
+ const brand = core.store.getBrand(brandId);
12849
+ if (!brand) return null;
12850
+ const json = brand.json;
12851
+ const characters = Array.isArray(json?.characters) ? json.characters : [];
12852
+ return {
12853
+ presenters: characters.filter((c) => c?.origin === "custom" && !c?.supersededBy).length,
12854
+ scenes: Array.isArray(json?.scenes) ? json.scenes.length : 0,
12855
+ products: core.catalog.listLibraryProducts(brandId, json).length
12856
+ };
12857
+ }
12858
+ var ownPresenter = (b) => (b.json?.characters ?? []).some((c) => c?.origin === "custom" && !c?.supersededBy);
12859
+ function evidence(core, m) {
12860
+ const { store } = core;
12861
+ switch (m) {
12862
+ case "shot":
12863
+ return store.hasFinishedNode("generation");
12864
+ case "refine":
12865
+ return store.hasFinishedNode("edit");
12866
+ case "presenter":
12867
+ return store.listBrands().some(ownPresenter);
12868
+ case "scene":
12869
+ return store.listBrands().some((b) => (b.json?.scenes ?? []).length > 0);
12870
+ case "product":
12871
+ return store.hasCatalogProduct() || store.listBrands().some((b) => (b.json?.products ?? []).length > 0);
12872
+ }
12873
+ }
12874
+ function load2(store) {
12875
+ return parse2(store.getSetting(KEY)) ?? blank(false);
12876
+ }
12877
+ function readGuide(core, env) {
12878
+ const r = load2(core.store);
12879
+ let changed = false;
12880
+ const at = (/* @__PURE__ */ new Date()).toISOString();
12881
+ for (const m of MILESTONES) {
12882
+ if (!r.done[m] && evidence(core, m)) {
12883
+ r.done[m] = at;
12884
+ changed = true;
12885
+ }
12886
+ }
12887
+ if (r.active && !core.store.getBrand(r.active.brandId)) {
12888
+ r.active = null;
12889
+ changed = true;
12890
+ }
12891
+ if (changed) write(core.store, r);
12892
+ const a = r.active;
12893
+ const nodeKind = a?.task === "first-shot" ? "generation" : a?.task === "refine" ? "edit" : null;
12894
+ const eligible = r.eligible && env.SCENRI_NO_GUIDE !== "1";
12895
+ return {
12896
+ eligible,
12897
+ welcome: r.welcome,
12898
+ hidden: r.hidden ?? !eligible,
12899
+ done: r.done,
12900
+ dismissed: r.dismissed,
12901
+ active: a,
12902
+ activeNodes: a && nodeKind ? core.store.nodesSince(a.brandId, nodeKind, a.since) : [],
12903
+ activeDraftId: a?.task === "presenter" ? core.store.presenterDraftSince(a.brandId, a.since) : null,
12904
+ counts: a ? countsOf(core, a.brandId) : null
12905
+ };
12906
+ }
12907
+ function applyIntent(core, body) {
12908
+ const b = body ?? {};
12909
+ const r = load2(core.store);
12910
+ if ("welcome" in b) {
12911
+ if (b.welcome !== "taken" && b.welcome !== "declined") return "welcome is taken or declined";
12912
+ r.welcome = b.welcome;
12913
+ } else if ("start" in b) {
12914
+ const s = b.start ?? {};
12915
+ if (!isTask(s.task)) return "unknown task";
12916
+ if (typeof s.brandId !== "string") return "brandId required";
12917
+ const baseline = countsOf(core, s.brandId);
12918
+ if (!baseline) return "brand not found";
12919
+ const held = r.active;
12920
+ if (held && held.paused && held.task === s.task && held.brandId === s.brandId) {
12921
+ const { paused: _, ...going } = held;
12922
+ r.active = going;
12923
+ } else r.active = { task: s.task, brandId: s.brandId, since: core.store.now(), baseline };
12924
+ r.dismissed = r.dismissed.filter((t) => t !== s.task);
12925
+ } else if ("finish" in b) {
12926
+ if (!isTask(b.finish)) return "unknown task";
12927
+ if (r.active?.task === b.finish) r.active = null;
12928
+ } else if ("dismiss" in b) {
12929
+ if (!isTask(b.dismiss)) return "unknown task";
12930
+ if (!r.dismissed.includes(b.dismiss)) r.dismissed.push(b.dismiss);
12931
+ if (r.active?.task === b.dismiss) r.active = { ...r.active, paused: true };
12932
+ } else if ("hidden" in b) {
12933
+ if (typeof b.hidden !== "boolean") return "hidden is true or false";
12934
+ r.hidden = b.hidden;
12935
+ } else return "unknown intent";
12936
+ write(core.store, r);
12937
+ return null;
12938
+ }
12939
+ function registerGuideRoutes(app, deps) {
12940
+ const { core } = deps;
12941
+ const env = deps.env ?? process.env;
12942
+ stampGuide(core.store, deps.version);
12943
+ app.get("/api/guide", async () => readGuide(core, env));
12944
+ app.post("/api/guide", async (req, reply) => {
12945
+ const error = applyIntent(core, req.body);
12946
+ if (error) return reply.status(400).send({ error });
12947
+ return readGuide(core, env);
12948
+ });
12949
+ }
12950
+
12768
12951
  // src/routes/desktop.ts
12769
12952
  function registerDesktopRoutes(app, deps) {
12770
12953
  const { core, runtime } = deps;
@@ -12779,7 +12962,7 @@ function registerDesktopRoutes(app, deps) {
12779
12962
  record: null
12780
12963
  };
12781
12964
  }
12782
- const { installDeps } = await import('./cli-3KKS7BE4.js');
12965
+ const { installDeps } = await import('./cli-GMRPOISO.js');
12783
12966
  const { desktopStatus } = await import('./install-VXJPLYWM.js');
12784
12967
  return desktopStatus(installDeps(runtime.entry));
12785
12968
  });
@@ -12787,7 +12970,7 @@ function registerDesktopRoutes(app, deps) {
12787
12970
  if (!runtime.entry) {
12788
12971
  return { ok: false, reason: "unsupported", message: "Desktop shortcuts are not available on this system yet." };
12789
12972
  }
12790
- const { addToDesktop } = await import('./cli-3KKS7BE4.js');
12973
+ const { addToDesktop } = await import('./cli-GMRPOISO.js');
12791
12974
  return addToDesktop(runtime.entry);
12792
12975
  });
12793
12976
  app.get("/api/desktop", async () => {
@@ -13080,7 +13263,7 @@ function buildServer(opts) {
13080
13263
  const { demoProducts } = loadDemoProducts(join(templatesRoot, "demo-products"));
13081
13264
  const demoProductById = demoProductResolver(demoProducts);
13082
13265
  registerDemoProductRoutes(app, { templatesRoot, demoProducts, demoProductById, thumbs });
13083
- registerShowcaseRoutes(app, { templatesRoot });
13266
+ registerShowcaseRoutes(app, { templatesRoot, thumbs });
13084
13267
  app.get("/api/formats", async () => FORMATS);
13085
13268
  function briefInputsOnly(brief) {
13086
13269
  const {
@@ -14260,6 +14443,7 @@ function buildServer(opts) {
14260
14443
  return drained;
14261
14444
  });
14262
14445
  registerSystemRoutes(app, { core, thumbs });
14446
+ registerGuideRoutes(app, { core, version: meta.version });
14263
14447
  registerDesktopRoutes(app, {
14264
14448
  core,
14265
14449
  runtime,
@@ -14427,7 +14611,7 @@ async function run() {
14427
14611
  }
14428
14612
  }
14429
14613
  const ownEntry = fileURLToPath(import.meta.url);
14430
- const { addToDesktop, installDeps } = await import('./cli-3KKS7BE4.js');
14614
+ const { addToDesktop, installDeps } = await import('./cli-GMRPOISO.js');
14431
14615
  const { refreshLauncher } = await import('./refresh-5HBDLLLB.js');
14432
14616
  const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-W6NJXMWT.js');
14433
14617
  const { launcherInstalled } = await import('./paths-6ZXRZUHY.js');
@@ -1,3 +1,3 @@
1
- export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug } from './chunk-UUYMUOZQ.js';
2
- //# sourceMappingURL=src-QZZDZM7I.js.map
3
- //# sourceMappingURL=src-QZZDZM7I.js.map
1
+ export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug } from './chunk-K6IELFL3.js';
2
+ //# sourceMappingURL=src-TC4UQ7H6.js.map
3
+ //# sourceMappingURL=src-TC4UQ7H6.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",