scenri 0.10.3 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.1](https://github.com/tonygorb/Scenri/compare/v0.11.0...v0.11.1) (2026-09-18)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * pause first-use guidance, and a bar panel opened from another stays open ([ad6fb8d](https://github.com/tonygorb/Scenri/commit/ad6fb8d3146f7e51ab257fdaab9c74cbef88dad8))
9
+ * **studio:** a bar panel opened while another is open stays open ([fb43b43](https://github.com/tonygorb/Scenri/commit/fb43b43c5e7cf44ce0bcd7a5dd3daf815b4c6875))
10
+ * **studio:** pause first-use guidance until it is finished ([6fd33f0](https://github.com/tonygorb/Scenri/commit/6fd33f074e2af8373a1ae8cdd7690468cc6e90dd))
11
+
12
+ ## [0.11.0](https://github.com/tonygorb/Scenri/compare/v0.10.3...v0.11.0) (2026-09-18)
13
+
14
+
15
+ ### Features
16
+
17
+ * first use, a tutor that walks the real task, with First steps and Learn ([47e9775](https://github.com/tonygorb/Scenri/commit/47e977569049803f0544ce19ad0289cdc8e187f5))
18
+ * Learn, every lesson in one place, begun and continued with the same tutor ([1d378e1](https://github.com/tonygorb/Scenri/commit/1d378e17f22a8ebf64878d67caa0996027e5f1f6))
19
+ * **studio:** New lands on Create with the add panel open ([674bf9c](https://github.com/tonygorb/Scenri/commit/674bf9cbb369443e75b78586210a0b0b12c94826))
20
+ * **studio:** New's plus rides a gold disc ([673a17b](https://github.com/tonygorb/Scenri/commit/673a17b1e48eb491ecc2c9a1d122955d11e24d0d))
21
+ * **studio:** the brand menu leads with where you have been and stays one height ([e28a857](https://github.com/tonygorb/Scenri/commit/e28a8573f797f75815687239cd58c1b4d01518e7))
22
+ * **studio:** the new top bar ([e1d262b](https://github.com/tonygorb/Scenri/commit/e1d262b9eb21a4fd788dae8acd487096223b87c6))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * 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))
28
+ * 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))
29
+ * closing Learn gives the keyboard back to Help when what opened it is gone ([c12d690](https://github.com/tonygorb/Scenri/commit/c12d690fad3e7ce4d1cfaed05aa6982a97d5de4c))
30
+ * in the walk a chip comes in through its ask and leaves only through Back ([a0e9fea](https://github.com/tonygorb/Scenri/commit/a0e9fea7426755ba020f766f0a0678df96542e2c))
31
+ * **studio:** a bar menu answers the next click, even while it is closing ([7b0af47](https://github.com/tonygorb/Scenri/commit/7b0af476d0af0691be60616687955c3521171787))
32
+ * **studio:** a brand icon that is its own ground fills its circle ([0f356fb](https://github.com/tonygorb/Scenri/commit/0f356fbbd19e899ae41751851f873fedbce4d9b5))
33
+ * **studio:** a brand's circle shows its icon, or its initial, never a shrunken wordmark ([6c8415d](https://github.com/tonygorb/Scenri/commit/6c8415da61b261629cee734366a1cece630a58c4))
34
+ * **studio:** New pressed with the add panel open leaves it open and focuses the brief ([37050a4](https://github.com/tonygorb/Scenri/commit/37050a4e4b99f431649f97423ab6596f8ba52dad))
35
+ * **studio:** New wears round ends, the mark shows its box, and Clear all joins its list ([2ed412e](https://github.com/tonygorb/Scenri/commit/2ed412e1faf4d60378b0996f02373ea804b27fe9))
36
+ * **studio:** New's plus sits on an ink disc, and the bar no longer shrinks ([86ceaa3](https://github.com/tonygorb/Scenri/commit/86ceaa33dc16856a1c3fa2e4950360d1a77c5eec))
37
+ * **studio:** pointing at New's plus lights the whole button ([d3a4973](https://github.com/tonygorb/Scenri/commit/d3a4973dc8ee9a0ce090e24fd3f3f8a591c1b0f5))
38
+ * **studio:** the bar wears the full Scenri lockup, bare ([25efd02](https://github.com/tonygorb/Scenri/commit/25efd0292c1d0982ec1f027364489306cf294bea))
39
+ * **studio:** the brand list ends on a whole row ([a20da23](https://github.com/tonygorb/Scenri/commit/a20da23fd89240fd533944ad222c62144795a12e))
40
+ * **studio:** the brand list fades into the line under it while there is more ([18073f8](https://github.com/tonygorb/Scenri/commit/18073f8ea67157c64bbb7b676eb9aade51276614))
41
+ * **studio:** the brand list is ten rows tall ([df5b4e1](https://github.com/tonygorb/Scenri/commit/df5b4e1b834aabf9b4dc22338e83ce99a19550e3))
42
+ * **studio:** the brand list runs straight into the line under it ([402e017](https://github.com/tonygorb/Scenri/commit/402e0176e3bb7ae5cc799af77de4852e92608ba1))
43
+ * **studio:** the brand menu is one compact list ([1e3dc70](https://github.com/tonygorb/Scenri/commit/1e3dc7002cae58fbc8bacc567cf3a924a38a6a1c))
44
+ * **studio:** the brand menu is one list, and the help button leaves this branch ([2e717d1](https://github.com/tonygorb/Scenri/commit/2e717d1474f45e625e85bef36b69d3397cb20925))
45
+ * 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))
46
+ * the welcome reads top to bottom, speaks plainly, and opens again from Help ([7edb94f](https://github.com/tonygorb/Scenri/commit/7edb94f289e17c1c4255cc6ef683758bc21aea17))
47
+
3
48
  ## [0.10.3](https://github.com/tonygorb/Scenri/compare/v0.10.2...v0.10.3) (2026-09-18)
4
49
 
5
50
 
@@ -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,43 @@ function registerImageRoutes(app, deps) {
11703
11704
 
11704
11705
  // src/release/notes.data.ts
11705
11706
  var RELEASES = [
11707
+ {
11708
+ version: "0.11.1",
11709
+ date: "2026-09-18",
11710
+ sections: [
11711
+ {
11712
+ heading: "Learn",
11713
+ body: "The welcome, the guided first shot, First steps and Learn are paused for now while they are finished. Nothing of them opens by itself, and Help no longer lists them."
11714
+ },
11715
+ {
11716
+ heading: "Fixes",
11717
+ body: "With a panel in the top bar open, pressing another control in the bar opens its panel and keeps it open, instead of opening it and closing it again at once."
11718
+ }
11719
+ ]
11720
+ },
11721
+ {
11722
+ version: "0.11.0",
11723
+ date: "2026-09-18",
11724
+ title: "A new top bar, and a tutor that makes your first shot with you.",
11725
+ sections: [
11726
+ {
11727
+ heading: "Top bar",
11728
+ 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."
11729
+ },
11730
+ {
11731
+ heading: "Brand",
11732
+ 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."
11733
+ },
11734
+ {
11735
+ heading: "Learn",
11736
+ 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."
11737
+ },
11738
+ {
11739
+ heading: "Fixes",
11740
+ 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."
11741
+ }
11742
+ ]
11743
+ },
11706
11744
  {
11707
11745
  version: "0.10.3",
11708
11746
  date: "2026-09-18",
@@ -12765,6 +12803,165 @@ function registerSystemRoutes(app, deps) {
12765
12803
  });
12766
12804
  }
12767
12805
 
12806
+ // src/routes/guide.ts
12807
+ var TASKS = ["first-shot", "refine", "product", "presenter", "scene"];
12808
+ var MILESTONES = ["shot", "refine", "product", "presenter", "scene"];
12809
+ var KEY = "guide";
12810
+ var isTask = (t) => TASKS.includes(t);
12811
+ var isMilestone = (m) => MILESTONES.includes(m);
12812
+ var num2 = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : 0;
12813
+ function blank(eligible) {
12814
+ return { v: 2, eligible, welcome: null, hidden: null, done: {}, dismissed: [], active: null };
12815
+ }
12816
+ function parse2(raw) {
12817
+ if (!raw) return null;
12818
+ try {
12819
+ const j = JSON.parse(raw);
12820
+ if (typeof j.eligible !== "boolean") return null;
12821
+ if (j.v !== 2) return null;
12822
+ const done = {};
12823
+ if (j.done && typeof j.done === "object") {
12824
+ for (const [k, at] of Object.entries(j.done)) if (isMilestone(k) && typeof at === "string") done[k] = at;
12825
+ }
12826
+ const a = j.active;
12827
+ const b = a?.baseline;
12828
+ const active = a && isTask(a.task) && typeof a.brandId === "string" && typeof a.since === "string" ? {
12829
+ task: a.task,
12830
+ brandId: a.brandId,
12831
+ since: a.since,
12832
+ baseline: { products: num2(b?.products), presenters: num2(b?.presenters), scenes: num2(b?.scenes) },
12833
+ ...a.paused === true ? { paused: true } : {}
12834
+ } : null;
12835
+ return {
12836
+ v: 2,
12837
+ eligible: j.eligible,
12838
+ welcome: j.welcome === "taken" || j.welcome === "declined" ? j.welcome : null,
12839
+ hidden: typeof j.hidden === "boolean" ? j.hidden : null,
12840
+ done,
12841
+ dismissed: Array.isArray(j.dismissed) ? j.dismissed.filter(isTask) : [],
12842
+ active
12843
+ };
12844
+ } catch {
12845
+ return null;
12846
+ }
12847
+ }
12848
+ function write(store, r) {
12849
+ store.setSetting(KEY, JSON.stringify(r));
12850
+ }
12851
+ function stampGuide(store, version) {
12852
+ if (store.getSetting(KEY) !== null) return;
12853
+ const eligible = store.listBrands().length === 0;
12854
+ if (eligible) {
12855
+ if (!store.getSetting("install.firstVersion")) store.setSetting("install.firstVersion", version);
12856
+ const seen = store.getSetting("whatsnew.seen");
12857
+ if (!seen || compareSemver(seen, version) < 0) store.setSetting("whatsnew.seen", version);
12858
+ }
12859
+ write(store, blank(eligible));
12860
+ }
12861
+ function countsOf(core, brandId) {
12862
+ const brand = core.store.getBrand(brandId);
12863
+ if (!brand) return null;
12864
+ const json = brand.json;
12865
+ const characters = Array.isArray(json?.characters) ? json.characters : [];
12866
+ return {
12867
+ presenters: characters.filter((c) => c?.origin === "custom" && !c?.supersededBy).length,
12868
+ scenes: Array.isArray(json?.scenes) ? json.scenes.length : 0,
12869
+ products: core.catalog.listLibraryProducts(brandId, json).length
12870
+ };
12871
+ }
12872
+ var ownPresenter = (b) => (b.json?.characters ?? []).some((c) => c?.origin === "custom" && !c?.supersededBy);
12873
+ function evidence(core, m) {
12874
+ const { store } = core;
12875
+ switch (m) {
12876
+ case "shot":
12877
+ return store.hasFinishedNode("generation");
12878
+ case "refine":
12879
+ return store.hasFinishedNode("edit");
12880
+ case "presenter":
12881
+ return store.listBrands().some(ownPresenter);
12882
+ case "scene":
12883
+ return store.listBrands().some((b) => (b.json?.scenes ?? []).length > 0);
12884
+ case "product":
12885
+ return store.hasCatalogProduct() || store.listBrands().some((b) => (b.json?.products ?? []).length > 0);
12886
+ }
12887
+ }
12888
+ function load2(store) {
12889
+ return parse2(store.getSetting(KEY)) ?? blank(false);
12890
+ }
12891
+ function readGuide(core, env) {
12892
+ const r = load2(core.store);
12893
+ let changed = false;
12894
+ const at = (/* @__PURE__ */ new Date()).toISOString();
12895
+ for (const m of MILESTONES) {
12896
+ if (!r.done[m] && evidence(core, m)) {
12897
+ r.done[m] = at;
12898
+ changed = true;
12899
+ }
12900
+ }
12901
+ if (r.active && !core.store.getBrand(r.active.brandId)) {
12902
+ r.active = null;
12903
+ changed = true;
12904
+ }
12905
+ if (changed) write(core.store, r);
12906
+ const a = r.active;
12907
+ const nodeKind = a?.task === "first-shot" ? "generation" : a?.task === "refine" ? "edit" : null;
12908
+ const eligible = r.eligible && env.SCENRI_NO_GUIDE !== "1";
12909
+ return {
12910
+ eligible,
12911
+ welcome: r.welcome,
12912
+ hidden: r.hidden ?? !eligible,
12913
+ done: r.done,
12914
+ dismissed: r.dismissed,
12915
+ active: a,
12916
+ activeNodes: a && nodeKind ? core.store.nodesSince(a.brandId, nodeKind, a.since) : [],
12917
+ activeDraftId: a?.task === "presenter" ? core.store.presenterDraftSince(a.brandId, a.since) : null,
12918
+ counts: a ? countsOf(core, a.brandId) : null
12919
+ };
12920
+ }
12921
+ function applyIntent(core, body) {
12922
+ const b = body ?? {};
12923
+ const r = load2(core.store);
12924
+ if ("welcome" in b) {
12925
+ if (b.welcome !== "taken" && b.welcome !== "declined") return "welcome is taken or declined";
12926
+ r.welcome = b.welcome;
12927
+ } else if ("start" in b) {
12928
+ const s = b.start ?? {};
12929
+ if (!isTask(s.task)) return "unknown task";
12930
+ if (typeof s.brandId !== "string") return "brandId required";
12931
+ const baseline = countsOf(core, s.brandId);
12932
+ if (!baseline) return "brand not found";
12933
+ const held = r.active;
12934
+ if (held && held.paused && held.task === s.task && held.brandId === s.brandId) {
12935
+ const { paused: _, ...going } = held;
12936
+ r.active = going;
12937
+ } else r.active = { task: s.task, brandId: s.brandId, since: core.store.now(), baseline };
12938
+ r.dismissed = r.dismissed.filter((t) => t !== s.task);
12939
+ } else if ("finish" in b) {
12940
+ if (!isTask(b.finish)) return "unknown task";
12941
+ if (r.active?.task === b.finish) r.active = null;
12942
+ } else if ("dismiss" in b) {
12943
+ if (!isTask(b.dismiss)) return "unknown task";
12944
+ if (!r.dismissed.includes(b.dismiss)) r.dismissed.push(b.dismiss);
12945
+ if (r.active?.task === b.dismiss) r.active = { ...r.active, paused: true };
12946
+ } else if ("hidden" in b) {
12947
+ if (typeof b.hidden !== "boolean") return "hidden is true or false";
12948
+ r.hidden = b.hidden;
12949
+ } else return "unknown intent";
12950
+ write(core.store, r);
12951
+ return null;
12952
+ }
12953
+ function registerGuideRoutes(app, deps) {
12954
+ const { core } = deps;
12955
+ const env = deps.env ?? process.env;
12956
+ stampGuide(core.store, deps.version);
12957
+ app.get("/api/guide", async () => readGuide(core, env));
12958
+ app.post("/api/guide", async (req, reply) => {
12959
+ const error = applyIntent(core, req.body);
12960
+ if (error) return reply.status(400).send({ error });
12961
+ return readGuide(core, env);
12962
+ });
12963
+ }
12964
+
12768
12965
  // src/routes/desktop.ts
12769
12966
  function registerDesktopRoutes(app, deps) {
12770
12967
  const { core, runtime } = deps;
@@ -12779,7 +12976,7 @@ function registerDesktopRoutes(app, deps) {
12779
12976
  record: null
12780
12977
  };
12781
12978
  }
12782
- const { installDeps } = await import('./cli-3KKS7BE4.js');
12979
+ const { installDeps } = await import('./cli-GMRPOISO.js');
12783
12980
  const { desktopStatus } = await import('./install-VXJPLYWM.js');
12784
12981
  return desktopStatus(installDeps(runtime.entry));
12785
12982
  });
@@ -12787,7 +12984,7 @@ function registerDesktopRoutes(app, deps) {
12787
12984
  if (!runtime.entry) {
12788
12985
  return { ok: false, reason: "unsupported", message: "Desktop shortcuts are not available on this system yet." };
12789
12986
  }
12790
- const { addToDesktop } = await import('./cli-3KKS7BE4.js');
12987
+ const { addToDesktop } = await import('./cli-GMRPOISO.js');
12791
12988
  return addToDesktop(runtime.entry);
12792
12989
  });
12793
12990
  app.get("/api/desktop", async () => {
@@ -13080,7 +13277,7 @@ function buildServer(opts) {
13080
13277
  const { demoProducts } = loadDemoProducts(join(templatesRoot, "demo-products"));
13081
13278
  const demoProductById = demoProductResolver(demoProducts);
13082
13279
  registerDemoProductRoutes(app, { templatesRoot, demoProducts, demoProductById, thumbs });
13083
- registerShowcaseRoutes(app, { templatesRoot });
13280
+ registerShowcaseRoutes(app, { templatesRoot, thumbs });
13084
13281
  app.get("/api/formats", async () => FORMATS);
13085
13282
  function briefInputsOnly(brief) {
13086
13283
  const {
@@ -14260,6 +14457,7 @@ function buildServer(opts) {
14260
14457
  return drained;
14261
14458
  });
14262
14459
  registerSystemRoutes(app, { core, thumbs });
14460
+ registerGuideRoutes(app, { core, version: meta.version });
14263
14461
  registerDesktopRoutes(app, {
14264
14462
  core,
14265
14463
  runtime,
@@ -14427,7 +14625,7 @@ async function run() {
14427
14625
  }
14428
14626
  }
14429
14627
  const ownEntry = fileURLToPath(import.meta.url);
14430
- const { addToDesktop, installDeps } = await import('./cli-3KKS7BE4.js');
14628
+ const { addToDesktop, installDeps } = await import('./cli-GMRPOISO.js');
14431
14629
  const { refreshLauncher } = await import('./refresh-5HBDLLLB.js');
14432
14630
  const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-W6NJXMWT.js');
14433
14631
  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.1",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",