@engenty/wizards-runtime 0.7.7 → 0.7.8

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/apps/runtime/dist/authoring/guide.js +8 -3
  2. package/apps/runtime/dist/browser/fast/classifier.js +9 -4
  3. package/apps/runtime/dist/cli/main.js +1 -1
  4. package/apps/runtime/dist/cli/menu.js +5 -1
  5. package/apps/runtime/dist/cli/ollama.js +302 -0
  6. package/apps/runtime/dist/cli/setup.js +102 -2
  7. package/apps/runtime/dist/decision-model.js +33 -0
  8. package/apps/runtime/dist/docs/user/command-line.md +1 -1
  9. package/apps/runtime/dist/docs/user/install.md +1 -0
  10. package/apps/runtime/dist/engine/decide.js +3 -1
  11. package/apps/runtime/dist/film/project.js +221 -65
  12. package/apps/runtime/dist/film/step.js +16 -5
  13. package/apps/runtime/dist/models.js +43 -3
  14. package/apps/runtime/dist/tools/code.js +53 -13
  15. package/apps/web/dist/assets/{AboutDialog-BP8-Ugxf.js → AboutDialog-ByZyHXDj.js} +1 -1
  16. package/apps/web/dist/assets/{ChangelogDialog-CD6CQNW9.js → ChangelogDialog-QHYP0jAv.js} +1 -1
  17. package/apps/web/dist/assets/{ChatView-4T0QarEr.js → ChatView-DupTtI8K.js} +1 -1
  18. package/apps/web/dist/assets/EditorPage-k6qouLeS.js +4 -0
  19. package/apps/web/dist/assets/{FlowPreview-Dj2gAfuf.js → FlowPreview-Dx8AjXvE.js} +1 -1
  20. package/apps/web/dist/assets/{Harness-SLbsntQF.js → Harness-NvIb36TG.js} +1 -1
  21. package/apps/web/dist/assets/{NewWizardChannels-CsDMlji7.js → NewWizardChannels-B2YgtJlt.js} +1 -1
  22. package/apps/web/dist/assets/{NewWizardPage-_bFYjlgh.js → NewWizardPage-BgLY6SL6.js} +1 -1
  23. package/apps/web/dist/assets/{OnboardingPage-DObQeCDg.js → OnboardingPage-CxhwefJU.js} +1 -1
  24. package/apps/web/dist/assets/{ProjectPage-DX-oNLWy.js → ProjectPage-6ceSSS7W.js} +1 -1
  25. package/apps/web/dist/assets/{PublicRunner-Bn9rVNEb.js → PublicRunner-6lQvReDx.js} +1 -1
  26. package/apps/web/dist/assets/{ReadOnly-CK0piLUV.js → ReadOnly-CGNHlgUT.js} +1 -1
  27. package/apps/web/dist/assets/{RunDrawer-CVMTLse_.js → RunDrawer-TVGNnWyx.js} +2 -2
  28. package/apps/web/dist/assets/{RunnerView-Cjhuu9ua.js → RunnerView-DInSK0VZ.js} +1 -1
  29. package/apps/web/dist/assets/SettingsPage-D3k5pXN5.js +1 -0
  30. package/apps/web/dist/assets/{SetupPage-D9jlhgiP.js → SetupPage-Bj5frdzY.js} +1 -1
  31. package/apps/web/dist/assets/{SharePage-Cc3Opj2F.js → SharePage-hG7Z6SLy.js} +1 -1
  32. package/apps/web/dist/assets/changelog-CO9vKjV0.js +1 -0
  33. package/apps/web/dist/assets/{esm-CfnFE_jE.js → esm-DVk3RJij.js} +1 -1
  34. package/apps/web/dist/assets/index-DfaoPjNW.css +1 -0
  35. package/apps/web/dist/assets/index-DlDsr9Lc.js +14 -0
  36. package/apps/web/dist/assets/{phone-BWOB67tX.js → phone-CXXHXYnA.js} +1 -1
  37. package/apps/web/dist/assets/{settings-menu-CEQlVs1O.js → settings-menu-DXzZCIMx.js} +1 -1
  38. package/apps/web/dist/assets/{ui-BmR_4eL2.js → ui-DQ4L2npj.js} +2 -2
  39. package/apps/web/dist/assets/{upload-pBKn2EFp.js → upload-BnYHIfu5.js} +1 -1
  40. package/apps/web/dist/assets/{lock-DAvRJY4B.js → wrench-BrUJ7SV6.js} +1 -1
  41. package/apps/web/dist/index.html +3 -3
  42. package/apps/web/dist/install.sh +2 -2
  43. package/apps/web/dist/mcp/flow.html +4 -4
  44. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +3 -0
  45. package/node_modules/@engenty-wizards/shared/dist/definition.js +15 -0
  46. package/package.json +1 -1
  47. package/apps/web/dist/assets/EditorPage-CWdV7Xdn.js +0 -3
  48. package/apps/web/dist/assets/SettingsPage-DTzuiSAj.js +0 -1
  49. package/apps/web/dist/assets/changelog-CJNkd1_L.js +0 -1
  50. package/apps/web/dist/assets/index-AfUa8xKD.css +0 -1
  51. package/apps/web/dist/assets/index-B5F1_qA_.js +0 -14
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
- import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, normalize } from "node:path";
5
5
  import { dataRef, FILM_SIZES } from "@engenty-wizards/shared/definition";
6
6
  import { gradientCss, ruleLines, textStyleLine } from "@engenty-wizards/shared/projects";
@@ -52,21 +52,43 @@ async function put(root, rel, data) {
52
52
  await writeFile(path, data);
53
53
  }
54
54
  const safeName = (name) => name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "file";
55
- function rules(step, skills) {
55
+ /** Where the client starts, by how film/ began. */
56
+ function startRules(start) {
57
+ if (start === "base") {
58
+ return `- **What to make:** BRIEF.md. **Start here:** film/ is already the wizard's base project in this
59
+ format — read film/PARTS.md first: its scene templates carry the look. Compose the film from them in
60
+ the one file PARTS.md names, timed to the voice; write a new scene only where no template fits, then
61
+ in the style of showcase/ (SHOWCASE.md says how each technique is made).
62
+ - **The brand:** film/brand/ holds brand.json and the logos. If film/brand/theme.json is missing, make
63
+ it first as film/BRAND.md says — it is kept for the next films of this space, so get it right.
64
+ Colours, logo and fonts come from there only.
65
+ `;
66
+ }
67
+ if (start === "from") {
68
+ return `- **What to make:** BRIEF.md. **Start here:** film/ holds a finished film of this run in another
69
+ format. Lay it out for this one: scenes, timing, words and sound stay; only the layout changes
70
+ (film/PARTS.md says how when it exists). The look is showcase/'s.
71
+ - **The brand:** film/brand/ when it is there, else brand/brand.json and brand/logos/ — colours, logo
72
+ and fonts come from there only.
73
+ `;
74
+ }
75
+ return `- **What to make:** BRIEF.md. **The bar:** showcase/ (read showcase/SHOWCASE.md and showcase/frame.md
76
+ first when it exists, look at its stills, reuse its helpers from showcase/assets/). **The brand:**
77
+ brand/brand.json and brand/logos/ — colours, logo and fonts come from there only.
78
+ `;
79
+ }
80
+ function rules(step, skills, start) {
56
81
  const size = FILM_SIZES[step.format];
57
82
  return `# The film folder
58
83
 
59
84
  You make one film here, autonomously, to the end. Nobody answers questions: where a skill asks the
60
85
  person, decide yourself and note it in PROGRESS.md.
61
86
 
62
- - **What to make:** BRIEF.md. **The bar:** showcase/ (read showcase/SHOWCASE.md and showcase/frame.md
63
- first when it exists, look at its stills, reuse its helpers from showcase/assets/). **The brand:**
64
- brand/brand.json and brand/logos/ — colours, logo and fonts come from there only.
65
- - **Material:** input/ (see input/README.md). Use nothing from the web; you have no network.
87
+ ${startRules(start)}- **Material:** input/ (see input/README.md). Use nothing from the web; you have no network.
66
88
  - **Skills:** ${skills.join(", ")} in .claude/skills/<name>/SKILL.md. Start with \`hyperframes\` and
67
89
  read the skill files directly; do not update or install skills.
68
- - **The HyperFrames project is film/** (${size.width}×${size.height}, already set up). Keep every file
69
- of the film inside film/.
90
+ - **The HyperFrames project is film/** (${size.width}×${size.height}). Keep every file of the film
91
+ inside film/.
70
92
  - **Running HyperFrames:** only through the \`hyperframes\` tool (MCP server \`film\`), e.g.
71
93
  \`["lint","film"]\`, \`["check","film"]\`, \`["snapshot","film","--at","1,4,8"]\`,
72
94
  \`["render","film","-q","draft"]\`, \`["transcribe","input/voice/l01.wav","--json","-d","input/voice"]\`.
@@ -86,57 +108,25 @@ person, decide yourself and note it in PROGRESS.md.
86
108
  - Install nothing (no pip, brew, npm).
87
109
  `;
88
110
  }
89
- /** Sets up the folder for a first attempt; a later attempt finds it as it was left. */
90
- export async function prepareFilmFolder(step, ctx) {
91
- const root = filmRoot(ctx.runId, step.id);
92
- const brief = renderTemplate(step.brief, ctx.scope).trim();
93
- // The same brief and material as last time: the film is there to go on with or to change.
94
- const stamp = createHash("sha256")
95
- .update(JSON.stringify([
96
- brief,
97
- Object.values(step.inputs).map((raw) => {
98
- const ref = dataRef(raw);
99
- const [head, id] = ref.split(".");
100
- return head === "steps" ? ctx.state.outputs[id]?.at : ctx.state.values[ref];
101
- }),
102
- ]))
103
- .digest("hex");
104
- const stampFile = join(root, ".engenty", "inputs");
105
- const same = existsSync(stampFile) && (await readFile(stampFile, "utf8")) === stamp;
106
- if (same && existsSync(join(root, "film", "index.html")) && (await savedSession(root))) {
107
- return { root, brief };
108
- }
109
- await rm(root, { recursive: true, force: true });
110
- await mkdir(root, { recursive: true });
111
- const skills = await placeSkills(step.skills, ctx.files, join(root, ".claude", "skills"));
112
- const size = FILM_SIZES[step.format];
113
- await put(root, "BRIEF.md", `# Brief\n\nFormat ${step.format}, ${size.width}×${size.height}${step.seconds ? `, about ${step.seconds} s` : ""}. Language of all text on screen: the brief's language.\n\n${brief}\n`);
114
- const text = rules(step, skills);
115
- await put(root, "CLAUDE.md", text);
116
- await put(root, "AGENTS.md", text);
117
- // The style kit.
118
- if (step.kit) {
119
- const file = ctx.files.find((f) => f.path === step.kit);
120
- if (file) {
121
- const entries = unzipSync(new Uint8Array(await getBlob(file.hash)));
122
- for (const [name, data] of Object.entries(entries)) {
123
- if (!name.endsWith("/")) {
124
- await put(join(root, "showcase"), name.replace(/^showcase\//, ""), data);
125
- }
126
- }
127
- }
128
- }
129
- // The space's brand.
111
+ const BRAND_STAMP = join(".engenty", "brand");
112
+ /**
113
+ * Writes the space's brand into brand/ as it is now (brand.json, logos). True when it differs
114
+ * from what the folder had before — the film's code then still carries the old one.
115
+ */
116
+ async function writeBrand(root, ctx) {
130
117
  const b = ctx.scope.brand;
131
- const logos = [];
118
+ const hash = createHash("sha256");
119
+ const files = [];
120
+ const fileName = (dir, f) => `${dir}/${safeName(f.name.replace(/\.[^.]+$/, ""))}.${extFor(f.mime) === "bin" ? (f.name.split(".").pop() ?? "bin") : extFor(f.mime)}`;
132
121
  const patterns = [];
133
122
  for (const p of b.patterns ?? []) {
134
123
  const f = ctx.projectFiles.find((x) => x.id === p.file);
135
124
  try {
136
125
  if (f) {
137
126
  const { data } = await projectFileContent(f.projectId, f.id);
138
- const name = `patterns/${safeName(f.name.replace(/\.[^.]+$/, ""))}.${extFor(f.mime) === "bin" ? (f.name.split(".").pop() ?? "bin") : extFor(f.mime)}`;
139
- await put(join(root, "brand"), name, data);
127
+ const name = fileName("patterns", f);
128
+ files.push({ name, data });
129
+ hash.update(name).update(data);
140
130
  patterns.push({
141
131
  file: name,
142
132
  name: p.name,
@@ -149,11 +139,13 @@ export async function prepareFilmFolder(step, ctx) {
149
139
  // a pattern less
150
140
  }
151
141
  }
142
+ const logos = [];
152
143
  for (const f of ctx.projectFiles.filter((p) => p.kind === "logo")) {
153
144
  try {
154
145
  const { data } = await projectFileContent(f.projectId, f.id);
155
- const name = `logos/${safeName(f.name.replace(/\.[^.]+$/, ""))}.${extFor(f.mime) === "bin" ? (f.name.split(".").pop() ?? "bin") : extFor(f.mime)}`;
156
- await put(join(root, "brand"), name, data);
146
+ const name = fileName("logos", f);
147
+ files.push({ name, data });
148
+ hash.update(name).update(data);
157
149
  const section = logoSectionLine(b.logoSections, f.id);
158
150
  logos.push({
159
151
  file: name,
@@ -165,7 +157,7 @@ export async function prepareFilmFolder(step, ctx) {
165
157
  // a logo less
166
158
  }
167
159
  }
168
- await put(join(root, "brand"), "brand.json", JSON.stringify({
160
+ const json = JSON.stringify({
169
161
  note: "The space's brand. Colours are named by what they are for; the first is the accent. A logo's section, a gradient's or a pattern's description says when to use it; colorRules, logoRules and typeRules, where there are any, say how colours, logos and type may be used; text is set in the fonts — a font with `uses` that leave out video is not used in a film: its text gets a system font.",
170
162
  name: b.name ?? "",
171
163
  about: b.about ?? "",
@@ -199,16 +191,149 @@ export async function prepareFilmFolder(step, ctx) {
199
191
  : {}),
200
192
  ...(b.textStyles?.length
201
193
  ? {
202
- textStyles: b.textStyles.map((s) => ({
203
- name: s.name,
204
- style: textStyleLine(s, b.fonts ?? []),
194
+ textStyles: b.textStyles.map((t) => ({
195
+ name: t.name,
196
+ style: textStyleLine(t, b.fonts ?? []),
205
197
  })),
206
198
  }
207
199
  : {}),
208
200
  ...(b.typeRules?.length ? { typeRules: ruleLines(b.typeRules).split("\n") } : {}),
209
201
  ...(b.logoRules?.length ? { logoRules: ruleLines(b.logoRules).split("\n") } : {}),
210
202
  facts: (b.facts ?? []).map((f) => `${f.label}: ${f.value}`),
211
- }, null, 1));
203
+ }, null, 1);
204
+ const stamp = hash.update(json).digest("hex");
205
+ let changed = false;
206
+ try {
207
+ changed = (await readFile(join(root, BRAND_STAMP), "utf8")) !== stamp;
208
+ }
209
+ catch {
210
+ // A folder from before the stamp: its brand.json tells; a new folder has none.
211
+ const old = await readFile(join(root, "brand", "brand.json"), "utf8").catch(() => null);
212
+ changed = old !== null && old !== json;
213
+ }
214
+ await rm(join(root, "brand"), { recursive: true, force: true });
215
+ for (const f of files) {
216
+ await put(join(root, "brand"), f.name, f.data);
217
+ }
218
+ await put(join(root, "brand"), "brand.json", json);
219
+ await put(root, BRAND_STAMP, stamp);
220
+ return { changed, stamp };
221
+ }
222
+ const START = join(".engenty", "start");
223
+ const BASE = join(".engenty", "base");
224
+ /**
225
+ * The brand base: what a film made once from the space's brand for the kit's base (the base's
226
+ * BRAND.md says what — theme.json at least), kept per space for the kit and the brand as they are.
227
+ * A changed brand or kit is another key, so it is made again.
228
+ */
229
+ function brandBaseDir(ctx, kitHash, brandStamp) {
230
+ const key = createHash("sha256").update(`${kitHash}:${brandStamp}`).digest("hex").slice(0, 24);
231
+ return join(env.dataDir, "brand-bases", ctx.project.id, key);
232
+ }
233
+ /** What in film/brand/ is the brand base: everything but the brand itself. */
234
+ const isBrandItself = (name) => name === "brand.json" || name === "logos" || name === "patterns";
235
+ /**
236
+ * Brings the brand into a project that started from a base: brand.json and the logos as they are
237
+ * now, and the brand base kept for them when there is one. A base that no longer fits the brand
238
+ * is removed — the film makes it again, as BRAND.md says.
239
+ */
240
+ async function brandIntoFilm(root, brandChanged) {
241
+ const base = await readFile(join(root, BASE), "utf8").catch(() => "");
242
+ if (!base) {
243
+ return;
244
+ }
245
+ const target = join(root, "film", "brand");
246
+ await mkdir(target, { recursive: true });
247
+ for (const name of await readdir(target)) {
248
+ if (isBrandItself(name) || (brandChanged && !existsSync(base))) {
249
+ await rm(join(target, name), { recursive: true, force: true });
250
+ }
251
+ }
252
+ await cp(join(root, "brand"), target, { recursive: true });
253
+ if (existsSync(base) && (brandChanged || !existsSync(join(target, "theme.json")))) {
254
+ await cp(base, target, { recursive: true });
255
+ }
256
+ }
257
+ /** A new brand is another brand base: the marker points at the one for the brand as it is now. */
258
+ async function refreshBrandBase(root, ctx, step) {
259
+ if (!existsSync(join(root, BASE))) {
260
+ return;
261
+ }
262
+ const kitHash = ctx.files.find((f) => f.path === step.kit)?.hash ?? "";
263
+ const stamp = await readFile(join(root, BRAND_STAMP), "utf8");
264
+ await put(root, BASE, brandBaseDir(ctx, kitHash, stamp));
265
+ }
266
+ /** After a film: keeps what it made from the brand for the next films in this space. */
267
+ export async function keepBrandBase(root) {
268
+ const base = await readFile(join(root, BASE), "utf8").catch(() => "");
269
+ const source = join(root, "film", "brand");
270
+ if (!base || !existsSync(join(source, "theme.json"))) {
271
+ return;
272
+ }
273
+ await rm(base, { recursive: true, force: true });
274
+ await mkdir(base, { recursive: true });
275
+ for (const name of await readdir(source)) {
276
+ if (!isBrandItself(name)) {
277
+ await cp(join(source, name), join(base, name), { recursive: true });
278
+ }
279
+ }
280
+ }
281
+ /** Sets up the folder for a first attempt; a later attempt finds it as it was left. */
282
+ export async function prepareFilmFolder(step, ctx) {
283
+ const root = filmRoot(ctx.runId, step.id);
284
+ const brief = renderTemplate(step.brief, ctx.scope).trim();
285
+ // An earlier film to start from, when it ran.
286
+ const fromId = step.from ? dataRef(step.from).replace(/^steps\./, "") : null;
287
+ const fromDir = fromId ? join(filmRoot(ctx.runId, fromId), "film") : null;
288
+ const from = fromId && fromDir && existsSync(join(fromDir, "index.html")) ? fromId : null;
289
+ // The same brief and material as last time: the film is there to go on with or to change.
290
+ const stamp = createHash("sha256")
291
+ .update(JSON.stringify([
292
+ brief,
293
+ Object.values(step.inputs).map((raw) => {
294
+ const ref = dataRef(raw);
295
+ const [head, id] = ref.split(".");
296
+ return head === "steps" ? ctx.state.outputs[id]?.at : ctx.state.values[ref];
297
+ }),
298
+ from ? ctx.state.outputs[from]?.at : null,
299
+ ]))
300
+ .digest("hex");
301
+ const stampFile = join(root, ".engenty", "inputs");
302
+ const same = existsSync(stampFile) && (await readFile(stampFile, "utf8")) === stamp;
303
+ if (same && existsSync(join(root, "film", "index.html")) && (await savedSession(root))) {
304
+ // The brand is the space's as it is now, also for a change: colours or a logo may be new.
305
+ const { changed } = await writeBrand(root, ctx);
306
+ const start = ((await readFile(join(root, START), "utf8").catch(() => "")) ||
307
+ "init");
308
+ if (changed) {
309
+ await refreshBrandBase(root, ctx, step);
310
+ }
311
+ await brandIntoFilm(root, changed);
312
+ return { root, brief, brandChanged: changed, start };
313
+ }
314
+ await rm(root, { recursive: true, force: true });
315
+ await mkdir(root, { recursive: true });
316
+ const skills = await placeSkills(step.skills, ctx.files, join(root, ".claude", "skills"));
317
+ const size = FILM_SIZES[step.format];
318
+ await put(root, "BRIEF.md", `# Brief\n\nFormat ${step.format}, ${size.width}×${size.height}${step.seconds ? `, about ${step.seconds} s` : ""}. Language of all text on screen: the brief's language.\n\n${brief}\n`);
319
+ // The style kit: the showcase to look at, and the base a film starts from when it has one.
320
+ const kitFile = step.kit ? ctx.files.find((f) => f.path === step.kit) : undefined;
321
+ const base = {};
322
+ if (kitFile) {
323
+ const entries = unzipSync(new Uint8Array(await getBlob(kitFile.hash)));
324
+ for (const [name, data] of Object.entries(entries)) {
325
+ if (name.endsWith("/")) {
326
+ continue;
327
+ }
328
+ if (name.startsWith("base/")) {
329
+ base[name.slice("base/".length)] = data;
330
+ }
331
+ else {
332
+ await put(join(root, "showcase"), name.replace(/^showcase\//, ""), data);
333
+ }
334
+ }
335
+ }
336
+ const { stamp: brandStamp } = await writeBrand(root, ctx);
212
337
  // What the run made before.
213
338
  const listed = [];
214
339
  for (const [name, raw] of Object.entries(step.inputs)) {
@@ -246,12 +371,43 @@ export async function prepareFilmFolder(step, ctx) {
246
371
  }
247
372
  await put(join(root, "input"), "README.md", `# Material for this film\n\n${listed.join("\n") || "(none)"}\n\nVoice lines play one after another in their order; their lengths decide the timing.\n`);
248
373
  await cp(await soundCatalog(), join(root, "audio-catalog"), { recursive: true });
249
- // The HyperFrames project, set up for the format.
250
- const preset = { "16:9": "landscape", "9:16": "portrait", "1:1": "square" }[step.format];
251
- const init = await runHyperframes(["init", "film", "--non-interactive", "--skip-transcribe", `--resolution=${preset}`], { cwd: root, signal: ctx.signal, timeoutMs: 5 * 60_000 });
252
- if (init.code !== 0 || !existsSync(join(root, "film"))) {
253
- throw new Error(`The film project could not be set up: ${init.out.slice(-300)}`);
374
+ // The HyperFrames project: the earlier film, the kit's base in this format, or a new one.
375
+ const index = `index.${step.format.replace(":", "x")}.html`;
376
+ let start;
377
+ if (from && fromDir) {
378
+ start = "from";
379
+ await cp(fromDir, join(root, "film"), {
380
+ recursive: true,
381
+ filter: (path) => !/\/(renders|snapshots)(\/|$)/.test(path.slice(fromDir.length)),
382
+ });
383
+ const fromBase = await readFile(join(filmRoot(ctx.runId, from), BASE), "utf8").catch(() => "");
384
+ if (fromBase) {
385
+ await put(root, BASE, fromBase);
386
+ }
254
387
  }
388
+ else if (base[index]) {
389
+ start = "base";
390
+ for (const [name, data] of Object.entries(base)) {
391
+ if (!/^index\.\d+x\d+\.html$/.test(name)) {
392
+ await put(join(root, "film"), name, data);
393
+ }
394
+ }
395
+ await put(join(root, "film"), "index.html", base[index]);
396
+ await put(root, BASE, brandBaseDir(ctx, kitFile?.hash ?? "", brandStamp));
397
+ }
398
+ else {
399
+ start = "init";
400
+ const preset = { "16:9": "landscape", "9:16": "portrait", "1:1": "square" }[step.format];
401
+ const init = await runHyperframes(["init", "film", "--non-interactive", "--skip-transcribe", `--resolution=${preset}`], { cwd: root, signal: ctx.signal, timeoutMs: 5 * 60_000 });
402
+ if (init.code !== 0 || !existsSync(join(root, "film"))) {
403
+ throw new Error(`The film project could not be set up: ${init.out.slice(-300)}`);
404
+ }
405
+ }
406
+ await brandIntoFilm(root, false);
407
+ const text = rules(step, skills, start);
408
+ await put(root, "CLAUDE.md", text);
409
+ await put(root, "AGENTS.md", text);
410
+ await put(root, START, start);
255
411
  await put(root, ".engenty/inputs", stamp);
256
- return { root, brief };
412
+ return { root, brief, brandChanged: false, start };
257
413
  }
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readFile, rm, stat } from "node:fs/promises";
4
4
  import { isAbsolute, join, normalize } from "node:path";
5
+ import { FILM_SIZES } from "@engenty-wizards/shared/definition";
5
6
  import { note as runNote } from "@engenty-wizards/shared/run";
6
7
  import { StepError } from "../engine/types.js";
7
8
  import { CLAUDE_BIN, claudeEnv } from "../harness/claude.js";
@@ -9,7 +10,7 @@ import { failureOf } from "../harness/model.js";
9
10
  import { openBridge } from "../mcp/bridge.js";
10
11
  import { saidOf } from "../model-errors.js";
11
12
  import { filmClient } from "../models.js";
12
- import { prepareFilmFolder, savedSession, saveSession } from "./project.js";
13
+ import { keepBrandBase, prepareFilmFolder, savedSession, saveSession } from "./project.js";
13
14
  import { runHyperframes } from "./tools.js";
14
15
  /**
15
16
  * A film step: the installed client (Claude Code) works in the film's folder with the skills,
@@ -99,7 +100,7 @@ export async function runFilmStep(step, ctx) {
99
100
  }
100
101
  const { client, alias } = found;
101
102
  await ctx.emit("info", { code: "filmSetup" });
102
- const { root, brief } = await prepareFilmFolder(step, ctx);
103
+ const { root, brief, brandChanged, start } = await prepareFilmFolder(step, ctx);
103
104
  const resume = await savedSession(root);
104
105
  await ctx.emit("info", resume ? { code: "filmResume" } : { code: "filmClient", params: { client: client.name } });
105
106
  const limit = AbortSignal.any([ctx.signal, AbortSignal.timeout(LIMIT_MS)]);
@@ -244,11 +245,20 @@ export async function runFilmStep(step, ctx) {
244
245
  try {
245
246
  // A review's note asks for a change: the film's code is there, so it is edited, not redone.
246
247
  const note = ctx.state.notes[step.id]?.trim();
248
+ // A brand changed in the space since the film was made: the film's code carries the old one.
249
+ const brandNote = !brandChanged
250
+ ? ""
251
+ : start === "init"
252
+ ? "\n\nThe space's brand has changed since the film was made: brand/brand.json and brand/logos/ are new. Carry them into the film wherever it uses the brand (its own copies included), so every colour, logo and name is the new one, and check the frames for contrast."
253
+ : "\n\nThe space's brand has changed since the film was made: film/brand/brand.json and the logos are new. If film/brand/theme.json is missing, make it again as film/BRAND.md says. Make sure every colour, logo and name in the film is the new one, and check the frames for contrast.";
254
+ const size = FILM_SIZES[step.format];
247
255
  const prompt = resume && note
248
- ? `The person watched the film and asks for this change:\n\n${note}\n\nChange the film in film/ accordingly — keep what they did not mention — check the frames again, and end with a short summary of what you changed.`
256
+ ? `The person watched the film and asks for this change:\n\n${note}${brandNote}\n\nChange the film in film/ accordingly — keep what they did not mention — check the frames again, and end with a short summary of what you changed.`
249
257
  : resume
250
- ? "Carry on with the film where you stopped. Read PROGRESS.md and the folder to see where that was, then finish it."
251
- : `Make the film BRIEF.md asks for, following CLAUDE.md.\n\n${brief}`;
258
+ ? `Carry on with the film where you stopped. Read PROGRESS.md and the folder to see where that was, then finish it.${brandNote}`
259
+ : start === "from"
260
+ ? `film/ holds this run's film in another format. Lay it out for ${step.format} (${size.width}×${size.height}), following CLAUDE.md: set the size of film/index.html, then make every scene fit — scenes, timing, words and sound stay. Check the frames, render a draft, and end with a short summary of what you changed.\n\nThe brief, for reference:\n\n${brief}`
261
+ : `Make the film BRIEF.md asks for, following CLAUDE.md.\n\n${brief}`;
252
262
  const first = await turn(prompt, resume);
253
263
  summary = typeof first.result === "string" ? first.result.trim() : "";
254
264
  // The final render is ours: it does not depend on the client still running. What fails
@@ -277,6 +287,7 @@ export async function runFilmStep(step, ctx) {
277
287
  finally {
278
288
  bridge.close();
279
289
  }
290
+ await keepBrandBase(root).catch(() => { });
280
291
  const ref = await ctx.saveAsset({
281
292
  stepId: step.id,
282
293
  kind: "video",
@@ -11,6 +11,7 @@ import { CAPABILITY_CLASSES, creditsModelOf, isCreditsRef, PROVIDER_IDS, provide
11
11
  import { note, saidText } from "@engenty-wizards/shared/run";
12
12
  import { generateSpeech, generateText, } from "ai";
13
13
  import { accountToken, linkedAccount } from "./auth/account.js";
14
+ import { ollamaDecisionModel, ollamaRoot } from "./decision-model.js";
14
15
  import { DeviceModel, deviceThinks } from "./engine/device.js";
15
16
  import { env } from "./env.js";
16
17
  import { appleModel, appleStatus, appleTranscriptionModel, appleUnavailableNote, } from "./harness/apple.js";
@@ -652,10 +653,37 @@ export async function embeddingModel() {
652
653
  ? { model: gatewayClient(access, {}).embeddingModel("wizards/embedding"), ref: bound.model }
653
654
  : null;
654
655
  }
656
+ /** What the local Ollama held at the last look, so a decision does not ask it every time. */
657
+ let localDecision = null;
658
+ /** The decision model in the local Ollama (Clef-flash, pulled by the setup), looked up once a minute. */
659
+ async function localDecisionModel(url) {
660
+ if (localDecision?.url === url && Date.now() < localDecision.at) {
661
+ return localDecision.model;
662
+ }
663
+ const model = await ollamaDecisionModel(url);
664
+ localDecision = { url, model, at: Date.now() + 60_000 };
665
+ return model;
666
+ }
667
+ /**
668
+ * Ollama refused the model itself, not the question: a newer Ollama that wants it downloaded
669
+ * again, a model that was removed, one that does not fit into memory. Decisions take the next
670
+ * way for ten minutes, and the log says what Ollama says fixes it.
671
+ */
672
+ function localModelRefused(url, status, body) {
673
+ const cannotRun = status === 404 ||
674
+ status >= 500 ||
675
+ /no longer supported|re-?download|ollama pull|not found/i.test(body);
676
+ if (!cannotRun) {
677
+ return;
678
+ }
679
+ console.warn(`[decide] the local decision model cannot run, left out for 10 minutes: ${body}`);
680
+ localDecision = { url, model: null, at: Date.now() + 10 * 60_000 };
681
+ }
655
682
  /**
656
- * The way to TypeSafe's Jev, a classifier that answers typed questions with probabilities and
657
- * writes no text: the credits on a Manage-App's runtime; alone, an AI Gateway key, a TypeSafe
658
- * key, or the linked account's credits. Null when there is none.
683
+ * The way to a decision model, one that answers typed questions with probabilities and writes
684
+ * no text: the credits on a Manage-App's runtime; alone, TypeSafe's Jev through an AI Gateway
685
+ * key or a TypeSafe key, then Clef-flash in Ollama on this machine, then Jev on the linked
686
+ * account's credits. Null when there is none.
659
687
  */
660
688
  export async function systemOneAccess() {
661
689
  const cfg = await config();
@@ -687,6 +715,18 @@ export async function systemOneAccess() {
687
715
  model: "jev-latest",
688
716
  };
689
717
  }
718
+ // Free, and nothing leaves the machine: before the credits.
719
+ const ollama = cfg.settings.ollamaUrl ?? env.ollamaUrl;
720
+ const local = await localDecisionModel(ollama);
721
+ if (local) {
722
+ return {
723
+ url: `${ollamaRoot(ollama)}/v1/systemone`,
724
+ headers: {},
725
+ model: local,
726
+ local: true,
727
+ refused: (status, body) => localModelRefused(ollama, status, body),
728
+ };
729
+ }
690
730
  const access = await creditAccess(cfg, false).catch(() => null);
691
731
  return access ? viaCredits(access) : null;
692
732
  }
@@ -17,8 +17,15 @@ export const CODE_LIMITS = {
17
17
  timeoutMs: 120_000,
18
18
  /** Tool calls one program may make. */
19
19
  maxCalls: 100,
20
+ /** Tool calls all programs of one step may make together: the model may run one program per turn. */
21
+ stepCalls: 300,
20
22
  /** Tool calls of one program running at the same time. */
21
23
  maxParallel: 4,
24
+ /**
25
+ * Characters of one tool answer a program takes in. QuickJS 0.31 can abort while it frees a
26
+ * runtime that parsed an answer of a million characters and more; this stays well below.
27
+ */
28
+ answerChars: 200_000,
22
29
  /** Characters of the program's answer the model reads. */
23
30
  resultChars: 20_000,
24
31
  /** Characters of `console.log` output the model reads. */
@@ -60,15 +67,23 @@ function limiter(max) {
60
67
  }
61
68
  };
62
69
  }
63
- /** What a program may call: tools the server runs itself (a provider's own search has no `execute`). */
70
+ /**
71
+ * Tools that cost credits on every call: a picture, or a model reading each document. A loop
72
+ * over them spends fast; the model calls them directly, where every call is its own decision.
73
+ */
74
+ export const PAID_TOOLS = new Set(["generate_image", "scan_documents"]);
75
+ /**
76
+ * What a program may call: tools the server runs itself (a provider's own search has no
77
+ * `execute`) that do not cost credits on every call.
78
+ */
64
79
  function callable(tools) {
65
- return Object.fromEntries(Object.entries(tools).filter(([, tool]) => typeof tool?.execute === "function"));
80
+ return Object.fromEntries(Object.entries(tools).filter(([key, tool]) => typeof tool?.execute === "function" && !PAID_TOOLS.has(key)));
66
81
  }
67
82
  /**
68
- * Wraps each tool for one program run: the call count and the calls in flight are capped, and the
69
- * tool knows it runs for a program (`inProgram`).
83
+ * Wraps each tool for one program run: the calls of the program and of the step, and the calls
84
+ * in flight are capped, and the tool knows it runs for a program (`inProgram`).
70
85
  */
71
- function guarded(tools, ctx, limits) {
86
+ function guarded(tools, ctx, limits, step) {
72
87
  let calls = 0;
73
88
  const slot = limiter(limits.maxParallel);
74
89
  return Object.fromEntries(Object.entries(tools).map(([key, tool]) => [
@@ -77,13 +92,24 @@ function guarded(tools, ctx, limits) {
77
92
  ...tool,
78
93
  execute: (input, options) => {
79
94
  calls++;
95
+ step.calls++;
80
96
  if (calls > limits.maxCalls) {
81
97
  throw new Error(`A program makes at most ${limits.maxCalls} tool calls. Split the work.`);
82
98
  }
99
+ if (step.calls > limits.stepCalls) {
100
+ throw new Error(`The programs of this step have made ${limits.stepCalls} tool calls, all a step may make. Finish with what you have.`);
101
+ }
83
102
  if (ctx.signal.aborted) {
84
103
  throw new Error("The run was stopped.");
85
104
  }
86
- return slot(() => program.run(true, () => tool.execute(input, options)));
105
+ return slot(async () => {
106
+ const answer = await program.run(true, () => tool.execute(input, options));
107
+ const size = JSON.stringify(answer ?? null)?.length ?? 0;
108
+ if (size > limits.answerChars) {
109
+ throw new Error(`${key} answered ${size} characters; a program takes at most ${limits.answerChars} from one call. Ask for less, or call ${key} directly.`);
110
+ }
111
+ return answer;
112
+ });
87
113
  },
88
114
  },
89
115
  ]));
@@ -98,6 +124,7 @@ export function codeTool(tools, ctx, limits = CODE_LIMITS) {
98
124
  return null;
99
125
  }
100
126
  const config = { id: CODE_TOOL_ID, tools: reachable, timeout: limits.timeoutMs };
127
+ const step = { calls: 0 };
101
128
  // The instructions list the tools once; every program runs over fresh guards.
102
129
  const { tool: base, instructions } = createCodeMode(config, quickJs());
103
130
  const tool = createTool({
@@ -106,12 +133,25 @@ export function codeTool(tools, ctx, limits = CODE_LIMITS) {
106
133
  inputSchema: base.inputSchema,
107
134
  execute: async (input, options) => {
108
135
  await ctx.emit("tool", { code: "runsProgram" });
109
- const run = createCodeMode({ ...config, tools: guarded(reachable, ctx, limits) }, quickJs()).tool;
110
- // Spans need a run being traced; a call without one still runs.
111
- const result = (await run.execute?.(input, {
112
- ...options,
113
- observe: options?.observe ?? noopObserve,
114
- }));
136
+ const run = createCodeMode({ ...config, tools: guarded(reachable, ctx, limits, step) }, quickJs()).tool;
137
+ let result;
138
+ try {
139
+ // Spans need a run being traced; a call without one still runs.
140
+ result = (await run.execute?.(input, {
141
+ ...options,
142
+ observe: options?.observe ?? noopObserve,
143
+ }));
144
+ }
145
+ catch (err) {
146
+ // QuickJS can fail while it frees a runtime that worked through a large tool answer; the
147
+ // program's result is lost, the next program runs as before.
148
+ return {
149
+ success: false,
150
+ error: {
151
+ message: `The program could not finish (${String(err?.message ?? err).slice(0, 120)}). Work on smaller pieces: return less from each step, or call the tool directly.`,
152
+ },
153
+ };
154
+ }
115
155
  const logs = result.logs?.length ? clip(result.logs.join("\n"), limits.logChars) : "";
116
156
  if (!result.success) {
117
157
  return { success: false, error: result.error, ...(logs ? { logs } : {}) };
@@ -128,6 +168,6 @@ export function codeTool(tools, ctx, limits = CODE_LIMITS) {
128
168
  });
129
169
  return {
130
170
  tool,
131
- instructions: `${instructions}\n\nA program makes at most ${limits.maxCalls} tool calls, ${limits.maxParallel} at a time, and stops after ${limits.timeoutMs / 1000} seconds. Return only what you need: a long answer is cut at ${limits.resultChars} characters. A tool that asks the person first cannot run from a program: call it directly.`,
171
+ instructions: `${instructions}\n\nA program makes at most ${limits.maxCalls} tool calls, ${limits.maxParallel} at a time, and stops after ${limits.timeoutMs / 1000} seconds; all programs of this step together make at most ${limits.stepCalls}. Return only what you need: a long answer is cut at ${limits.resultChars} characters. A tool that asks the person first cannot run from a program, and tools that cost credits on every call (${[...PAID_TOOLS].join(", ")}) are not offered there: call them directly.`,
132
172
  };
133
173
  }
@@ -1 +1 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{n as t,t as n}from"./jsx-runtime-BNakU3Ej.js";import{$t as r,Ft as i,It as a,Kt as o,Nt as s,Pt as c,Vn as l,Vt as u,Wt as d,_r as f,dr as p}from"./ui-BmR_4eL2.js";import{t as m}from"./preload-helper-CNEI7N0N.js";import{Nt as h,nt as g,rt as _,tt as v}from"./index-B5F1_qA_.js";import{c as y,f as b}from"./settings-menu-CEQlVs1O.js";import{ReleaseNotes as x,loadReleases as S,releaseDate as C,releaseLabel as w}from"./ChangelogDialog-CD6CQNW9.js";var T=e(t(),1),E=[{name:`engenty wizards`,description:`credit.engenty-wizards`,homepage:`https://github.com/engenty/wizards`,license:`FSL-1.1-MIT`},{name:`engenty`,description:`credit.engenty`,homepage:`https://github.com/engenty/engenty`,license:`FSL-1.1-MIT`}],D=[{name:`Mastra`,description:`credit.mastra`,homepage:`https://github.com/mastra-ai/mastra`,license:`Apache-2.0`},{name:`AI SDK`,description:`credit.ai-sdk`,homepage:`https://github.com/vercel/ai`,license:`Apache-2.0`},{name:`Model Context Protocol`,description:`credit.model-context-protocol`,homepage:`https://github.com/modelcontextprotocol/typescript-sdk`,license:`MIT`},{name:`Hono`,description:`credit.hono`,homepage:`https://github.com/honojs/hono`,license:`MIT`},{name:`Drizzle ORM`,description:`credit.drizzle-orm`,homepage:`https://github.com/drizzle-team/drizzle-orm`,license:`Apache-2.0`},{name:`libSQL`,description:`credit.libsql`,homepage:`https://github.com/tursodatabase/libsql`,license:`MIT`},{name:`Playwright`,description:`credit.playwright`,homepage:`https://github.com/microsoft/playwright`,license:`Apache-2.0`},{name:`React`,description:`credit.react`,homepage:`https://github.com/facebook/react`,license:`MIT`},{name:`Vite`,description:`credit.vite`,homepage:`https://github.com/vitejs/vite`,license:`MIT`},{name:`Tailwind CSS`,description:`credit.tailwind-css`,homepage:`https://github.com/tailwindlabs/tailwindcss`,license:`MIT`},{name:`React Flow`,description:`credit.react-flow`,homepage:`https://github.com/xyflow/xyflow`,license:`MIT`},{name:`TanStack Query`,description:`credit.tanstack-query`,homepage:`https://github.com/TanStack/query`,license:`MIT`},{name:`xterm.js`,description:`credit.xterm-js`,homepage:`https://github.com/xtermjs/xterm.js`,license:`MIT`},{name:`Lucide`,description:`credit.lucide`,homepage:`https://github.com/lucide-icons/lucide`,license:`ISC`},{name:`Geist & Geist Mono`,description:`credit.geist-geist-mono`,homepage:`https://github.com/vercel/geist-font`,license:`OFL-1.1`},{name:`Space Grotesk`,description:`credit.space-grotesk`,homepage:`https://github.com/floriankarsten/space-grotesk`,license:`OFL-1.1`},{name:`git-cliff`,description:`credit.git-cliff`,homepage:`https://github.com/orhun/git-cliff`,license:`MIT OR Apache-2.0`}],O=e(n(),1),k=(e,...t)=>t.some(t=>t?.toLowerCase().includes(e));function A({name:e,homepage:t,version:n,license:r,description:i}){return(0,O.jsxs)(`li`,{className:`py-2.5`,children:[(0,O.jsxs)(`div`,{className:`flex flex-wrap items-baseline gap-x-2.5 gap-y-0.5`,children:[t?(0,O.jsxs)(`a`,{href:t,target:`_blank`,rel:`noreferrer`,className:`inline-flex items-center gap-1 font-medium text-[0.875rem] text-ink underline-offset-4 hover:underline`,children:[e,(0,O.jsx)(p,{"aria-hidden":!0,className:`size-3 text-ink-4`})]}):(0,O.jsx)(`span`,{className:`font-medium text-[0.875rem] text-ink`,children:e}),n?(0,O.jsx)(`span`,{className:`text-[0.75rem] text-ink-3 tabular-nums`,children:n}):null,(0,O.jsx)(`span`,{className:`text-[0.75rem] text-ink-3`,children:r})]}),i?(0,O.jsx)(`p`,{className:`mt-0.5 text-[0.8125rem] text-ink-3`,children:i}):null]})}function j({title:e,hint:t,children:n}){return(0,O.jsxs)(`section`,{className:`mt-6`,children:[(0,O.jsx)(`h3`,{className:`border-border-soft border-b pb-2 font-display font-semibold text-[1rem]`,children:e}),t?(0,O.jsx)(`p`,{className:`mt-2 text-[0.8125rem] text-ink-3`,children:t}):null,n]})}function M({credits:e}){return(0,O.jsx)(`ul`,{className:`divide-y divide-border-soft`,children:e.map(e=>(0,O.jsx)(A,{name:e.name,homepage:e.homepage,license:e.license,description:r(e.description)},e.name))})}function N({label:e,packages:t}){return(0,O.jsxs)(`div`,{className:`mt-4`,children:[(0,O.jsx)(`h4`,{className:`font-medium text-[0.75rem] text-ink-3 tracking-wide`,children:e}),(0,O.jsx)(`ul`,{className:`divide-y divide-border-soft`,children:t.map(e=>(0,O.jsx)(A,{...e},e.name))})]})}function P({active:e}){let[t,n]=(0,T.useState)(null),[i,o]=(0,T.useState)(``);(0,T.useEffect)(()=>{if(!e||t)return;let r=!1;return m(()=>import(`./oss-credits-X2JPiMOR.js`).then(e=>{r||n(e.default)}),[]),()=>{r=!0}},[e,t]),(0,T.useEffect)(()=>{e||o(``)},[e]);let s=i.trim().toLowerCase(),l=e=>s?e.filter(e=>k(s,e.name,e.license,r(e.description))):e,d=e=>s?e.filter(e=>k(s,e.name,e.license,e.version)):e,f=l(E),p=l(D),h=t?[{id:`shared`,label:r(`about.creditsShared`),packages:d(t.shared)},...t.groups.map(e=>({id:e.id,label:e.label,packages:d(e.packages)}))].filter(e=>e.packages.length>0):[],g=f.length===0&&p.length===0&&h.length===0;return(0,O.jsxs)(O.Fragment,{children:[(0,O.jsx)(`p`,{className:`mb-4 text-[0.875rem] text-ink-3`,children:r(`about.creditsHint`)}),(0,O.jsx)(a,{type:`search`,value:i,placeholder:r(`about.search`),onChange:e=>o(e.target.value)}),f.length>0?(0,O.jsx)(j,{title:r(`about.creditsProduct`),children:(0,O.jsx)(M,{credits:f})}):null,p.length>0?(0,O.jsx)(j,{title:r(`about.creditsBuiltWith`),children:(0,O.jsx)(M,{credits:p})}):null,t===null?(0,O.jsx)(`div`,{className:`flex justify-center py-10 text-ink-3`,children:(0,O.jsx)(u,{})}):h.length>0?(0,O.jsx)(j,{title:r(`about.creditsDeps`),hint:s?void 0:r(`about.creditsDepsHint`),children:h.map(e=>(0,O.jsx)(N,{label:e.label,packages:e.packages},e.id))}):null,t!==null&&g?(0,O.jsx)(c,{children:r(`about.noMatches`)}):null]})}var F=[`new`,`earlier`,`credits`],I=e=>r(e===`new`?`about.tab.new`:e===`earlier`?`about.tab.earlier`:`about.credits`),L=e=>e===1?r(`about.change`):r(`about.changes`,{n:e});function R({open:e,onClose:t,me:n}){let a=n?.mode===`local`,{data:c,latest:p}=_(a&&e),[m,E]=(0,T.useState)(null),[D,k]=(0,T.useState)(`new`),A=b(F.indexOf(D)),j=c?.newer&&p?p:g;(0,T.useEffect)(()=>{if(!e)return;let t=!1;return E(null),S(j===`0.7.7`?void 0:j).then(e=>{t||E(e)}),()=>{t=!0}},[e,j]);let M=m?.find(e=>e.version===`v${j}`)??m?.[0]??null,N=m?.filter(e=>e!==M)??[],R=a?c?.kind??null:`cloud`;return(0,O.jsxs)(s,{open:e,onClose:t,wide:!0,height:`tall`,children:[(0,O.jsx)(`div`,{className:`-mt-2 -mr-2 sticky top-0 z-10 flex h-0 justify-end`,children:(0,O.jsx)(i,{label:r(`common.close`),onClick:t,children:(0,O.jsx)(l,{className:`size-4`})})}),(0,O.jsxs)(`div`,{className:`flex flex-col items-center pt-4 text-center`,children:[(0,O.jsx)(h,{size:96}),(0,O.jsxs)(`div`,{className:`mt-4 font-display font-semibold text-[1.5rem] tracking-tight`,children:[`engenty`,(0,O.jsx)(`span`,{className:`text-ember`,children:`.`}),(0,O.jsx)(`span`,{className:`ml-1.5 font-normal text-ink-3`,children:`wizards`})]}),(0,O.jsxs)(`div`,{className:`mt-1 text-[0.9375rem] text-ink-3 tabular-nums`,children:[r(`about.version`,{v:g}),R?` · ${r(`about.kind.${R}`)}`:``]})]}),a?(0,O.jsx)(`div`,{className:`mt-5`,children:(0,O.jsx)(v,{enabled:e})}):null,(0,O.jsx)(`div`,{className:`mt-6 flex justify-center`,children:(0,O.jsx)(d,{value:D,onChange:k,options:F.map(e=>({id:e,label:I(e)})),label:r(`about.title`)})}),(0,O.jsxs)(`div`,{className:o(`mt-5`,y(A)),children:[D===`new`?m===null?(0,O.jsx)(`div`,{className:`flex justify-center py-6 text-ink-3`,children:(0,O.jsx)(u,{})}):M?(0,O.jsxs)(O.Fragment,{children:[(0,O.jsxs)(`h3`,{className:`flex items-baseline gap-3 border-border-soft border-b pb-2 font-display font-semibold text-[1rem]`,children:[w(M),(0,O.jsx)(`span`,{className:`font-normal font-sans text-[0.8125rem] text-ink-3 tabular-nums`,children:C(M)})]}),(0,O.jsx)(`div`,{className:`mt-3`,children:(0,O.jsx)(x,{release:M})})]}):(0,O.jsx)(`p`,{className:`text-[0.875rem] text-ink-3`,children:r(`about.changelogEmpty`)}):null,D===`earlier`?(0,O.jsx)(`div`,{className:`flex flex-col divide-y divide-border-soft`,children:N.map(e=>(0,O.jsxs)(`details`,{className:`group/r py-2`,children:[(0,O.jsxs)(`summary`,{className:`flex cursor-pointer list-none items-baseline gap-3 text-[0.875rem] [&::-webkit-details-marker]:hidden`,children:[(0,O.jsx)(f,{className:`size-3.5 shrink-0 self-center text-ink-4 transition group-open/r:rotate-90`}),(0,O.jsx)(`span`,{className:`font-medium text-ink tabular-nums`,children:w(e)}),(0,O.jsx)(`span`,{className:`text-[0.8125rem] text-ink-3 tabular-nums`,children:C(e)}),(0,O.jsx)(`span`,{className:`ml-auto text-[0.8125rem] text-ink-4`,children:L(e.commits.length)})]}),(0,O.jsx)(`div`,{className:`mt-2 pl-6`,children:(0,O.jsx)(x,{release:e})})]},e.version??`unreleased`))}):null,D===`credits`?(0,O.jsx)(P,{active:!0}):null]},D)]})}export{R as AboutDialog};
1
+ import{r as e}from"./rolldown-runtime-hePW80VL.js";import{n as t,t as n}from"./jsx-runtime-BNakU3Ej.js";import{$t as r,Ft as i,It as a,Kt as o,Nt as s,Pt as c,Vn as l,Vt as u,Wt as d,_r as f,dr as p}from"./ui-DQ4L2npj.js";import{t as m}from"./preload-helper-CNEI7N0N.js";import{Nt as h,nt as g,rt as _,tt as v}from"./index-DlDsr9Lc.js";import{c as y,f as b}from"./settings-menu-DXzZCIMx.js";import{ReleaseNotes as x,loadReleases as S,releaseDate as C,releaseLabel as w}from"./ChangelogDialog-QHYP0jAv.js";var T=e(t(),1),E=[{name:`engenty wizards`,description:`credit.engenty-wizards`,homepage:`https://github.com/engenty/wizards`,license:`FSL-1.1-MIT`},{name:`engenty`,description:`credit.engenty`,homepage:`https://github.com/engenty/engenty`,license:`FSL-1.1-MIT`}],D=[{name:`Mastra`,description:`credit.mastra`,homepage:`https://github.com/mastra-ai/mastra`,license:`Apache-2.0`},{name:`AI SDK`,description:`credit.ai-sdk`,homepage:`https://github.com/vercel/ai`,license:`Apache-2.0`},{name:`Model Context Protocol`,description:`credit.model-context-protocol`,homepage:`https://github.com/modelcontextprotocol/typescript-sdk`,license:`MIT`},{name:`Hono`,description:`credit.hono`,homepage:`https://github.com/honojs/hono`,license:`MIT`},{name:`Drizzle ORM`,description:`credit.drizzle-orm`,homepage:`https://github.com/drizzle-team/drizzle-orm`,license:`Apache-2.0`},{name:`libSQL`,description:`credit.libsql`,homepage:`https://github.com/tursodatabase/libsql`,license:`MIT`},{name:`Playwright`,description:`credit.playwright`,homepage:`https://github.com/microsoft/playwright`,license:`Apache-2.0`},{name:`React`,description:`credit.react`,homepage:`https://github.com/facebook/react`,license:`MIT`},{name:`Vite`,description:`credit.vite`,homepage:`https://github.com/vitejs/vite`,license:`MIT`},{name:`Tailwind CSS`,description:`credit.tailwind-css`,homepage:`https://github.com/tailwindlabs/tailwindcss`,license:`MIT`},{name:`React Flow`,description:`credit.react-flow`,homepage:`https://github.com/xyflow/xyflow`,license:`MIT`},{name:`TanStack Query`,description:`credit.tanstack-query`,homepage:`https://github.com/TanStack/query`,license:`MIT`},{name:`xterm.js`,description:`credit.xterm-js`,homepage:`https://github.com/xtermjs/xterm.js`,license:`MIT`},{name:`Lucide`,description:`credit.lucide`,homepage:`https://github.com/lucide-icons/lucide`,license:`ISC`},{name:`Geist & Geist Mono`,description:`credit.geist-geist-mono`,homepage:`https://github.com/vercel/geist-font`,license:`OFL-1.1`},{name:`Space Grotesk`,description:`credit.space-grotesk`,homepage:`https://github.com/floriankarsten/space-grotesk`,license:`OFL-1.1`},{name:`git-cliff`,description:`credit.git-cliff`,homepage:`https://github.com/orhun/git-cliff`,license:`MIT OR Apache-2.0`}],O=e(n(),1),k=(e,...t)=>t.some(t=>t?.toLowerCase().includes(e));function A({name:e,homepage:t,version:n,license:r,description:i}){return(0,O.jsxs)(`li`,{className:`py-2.5`,children:[(0,O.jsxs)(`div`,{className:`flex flex-wrap items-baseline gap-x-2.5 gap-y-0.5`,children:[t?(0,O.jsxs)(`a`,{href:t,target:`_blank`,rel:`noreferrer`,className:`inline-flex items-center gap-1 font-medium text-[0.875rem] text-ink underline-offset-4 hover:underline`,children:[e,(0,O.jsx)(p,{"aria-hidden":!0,className:`size-3 text-ink-4`})]}):(0,O.jsx)(`span`,{className:`font-medium text-[0.875rem] text-ink`,children:e}),n?(0,O.jsx)(`span`,{className:`text-[0.75rem] text-ink-3 tabular-nums`,children:n}):null,(0,O.jsx)(`span`,{className:`text-[0.75rem] text-ink-3`,children:r})]}),i?(0,O.jsx)(`p`,{className:`mt-0.5 text-[0.8125rem] text-ink-3`,children:i}):null]})}function j({title:e,hint:t,children:n}){return(0,O.jsxs)(`section`,{className:`mt-6`,children:[(0,O.jsx)(`h3`,{className:`border-border-soft border-b pb-2 font-display font-semibold text-[1rem]`,children:e}),t?(0,O.jsx)(`p`,{className:`mt-2 text-[0.8125rem] text-ink-3`,children:t}):null,n]})}function M({credits:e}){return(0,O.jsx)(`ul`,{className:`divide-y divide-border-soft`,children:e.map(e=>(0,O.jsx)(A,{name:e.name,homepage:e.homepage,license:e.license,description:r(e.description)},e.name))})}function N({label:e,packages:t}){return(0,O.jsxs)(`div`,{className:`mt-4`,children:[(0,O.jsx)(`h4`,{className:`font-medium text-[0.75rem] text-ink-3 tracking-wide`,children:e}),(0,O.jsx)(`ul`,{className:`divide-y divide-border-soft`,children:t.map(e=>(0,O.jsx)(A,{...e},e.name))})]})}function P({active:e}){let[t,n]=(0,T.useState)(null),[i,o]=(0,T.useState)(``);(0,T.useEffect)(()=>{if(!e||t)return;let r=!1;return m(()=>import(`./oss-credits-X2JPiMOR.js`).then(e=>{r||n(e.default)}),[]),()=>{r=!0}},[e,t]),(0,T.useEffect)(()=>{e||o(``)},[e]);let s=i.trim().toLowerCase(),l=e=>s?e.filter(e=>k(s,e.name,e.license,r(e.description))):e,d=e=>s?e.filter(e=>k(s,e.name,e.license,e.version)):e,f=l(E),p=l(D),h=t?[{id:`shared`,label:r(`about.creditsShared`),packages:d(t.shared)},...t.groups.map(e=>({id:e.id,label:e.label,packages:d(e.packages)}))].filter(e=>e.packages.length>0):[],g=f.length===0&&p.length===0&&h.length===0;return(0,O.jsxs)(O.Fragment,{children:[(0,O.jsx)(`p`,{className:`mb-4 text-[0.875rem] text-ink-3`,children:r(`about.creditsHint`)}),(0,O.jsx)(a,{type:`search`,value:i,placeholder:r(`about.search`),onChange:e=>o(e.target.value)}),f.length>0?(0,O.jsx)(j,{title:r(`about.creditsProduct`),children:(0,O.jsx)(M,{credits:f})}):null,p.length>0?(0,O.jsx)(j,{title:r(`about.creditsBuiltWith`),children:(0,O.jsx)(M,{credits:p})}):null,t===null?(0,O.jsx)(`div`,{className:`flex justify-center py-10 text-ink-3`,children:(0,O.jsx)(u,{})}):h.length>0?(0,O.jsx)(j,{title:r(`about.creditsDeps`),hint:s?void 0:r(`about.creditsDepsHint`),children:h.map(e=>(0,O.jsx)(N,{label:e.label,packages:e.packages},e.id))}):null,t!==null&&g?(0,O.jsx)(c,{children:r(`about.noMatches`)}):null]})}var F=[`new`,`earlier`,`credits`],I=e=>r(e===`new`?`about.tab.new`:e===`earlier`?`about.tab.earlier`:`about.credits`),L=e=>e===1?r(`about.change`):r(`about.changes`,{n:e});function R({open:e,onClose:t,me:n}){let a=n?.mode===`local`,{data:c,latest:p}=_(a&&e),[m,E]=(0,T.useState)(null),[D,k]=(0,T.useState)(`new`),A=b(F.indexOf(D)),j=c?.newer&&p?p:g;(0,T.useEffect)(()=>{if(!e)return;let t=!1;return E(null),S(j===`0.7.8`?void 0:j).then(e=>{t||E(e)}),()=>{t=!0}},[e,j]);let M=m?.find(e=>e.version===`v${j}`)??m?.[0]??null,N=m?.filter(e=>e!==M)??[],R=a?c?.kind??null:`cloud`;return(0,O.jsxs)(s,{open:e,onClose:t,wide:!0,height:`tall`,children:[(0,O.jsx)(`div`,{className:`-mt-2 -mr-2 sticky top-0 z-10 flex h-0 justify-end`,children:(0,O.jsx)(i,{label:r(`common.close`),onClick:t,children:(0,O.jsx)(l,{className:`size-4`})})}),(0,O.jsxs)(`div`,{className:`flex flex-col items-center pt-4 text-center`,children:[(0,O.jsx)(h,{size:96}),(0,O.jsxs)(`div`,{className:`mt-4 font-display font-semibold text-[1.5rem] tracking-tight`,children:[`engenty`,(0,O.jsx)(`span`,{className:`text-ember`,children:`.`}),(0,O.jsx)(`span`,{className:`ml-1.5 font-normal text-ink-3`,children:`wizards`})]}),(0,O.jsxs)(`div`,{className:`mt-1 text-[0.9375rem] text-ink-3 tabular-nums`,children:[r(`about.version`,{v:g}),R?` · ${r(`about.kind.${R}`)}`:``]})]}),a?(0,O.jsx)(`div`,{className:`mt-5`,children:(0,O.jsx)(v,{enabled:e})}):null,(0,O.jsx)(`div`,{className:`mt-6 flex justify-center`,children:(0,O.jsx)(d,{value:D,onChange:k,options:F.map(e=>({id:e,label:I(e)})),label:r(`about.title`)})}),(0,O.jsxs)(`div`,{className:o(`mt-5`,y(A)),children:[D===`new`?m===null?(0,O.jsx)(`div`,{className:`flex justify-center py-6 text-ink-3`,children:(0,O.jsx)(u,{})}):M?(0,O.jsxs)(O.Fragment,{children:[(0,O.jsxs)(`h3`,{className:`flex items-baseline gap-3 border-border-soft border-b pb-2 font-display font-semibold text-[1rem]`,children:[w(M),(0,O.jsx)(`span`,{className:`font-normal font-sans text-[0.8125rem] text-ink-3 tabular-nums`,children:C(M)})]}),(0,O.jsx)(`div`,{className:`mt-3`,children:(0,O.jsx)(x,{release:M})})]}):(0,O.jsx)(`p`,{className:`text-[0.875rem] text-ink-3`,children:r(`about.changelogEmpty`)}):null,D===`earlier`?(0,O.jsx)(`div`,{className:`flex flex-col divide-y divide-border-soft`,children:N.map(e=>(0,O.jsxs)(`details`,{className:`group/r py-2`,children:[(0,O.jsxs)(`summary`,{className:`flex cursor-pointer list-none items-baseline gap-3 text-[0.875rem] [&::-webkit-details-marker]:hidden`,children:[(0,O.jsx)(f,{className:`size-3.5 shrink-0 self-center text-ink-4 transition group-open/r:rotate-90`}),(0,O.jsx)(`span`,{className:`font-medium text-ink tabular-nums`,children:w(e)}),(0,O.jsx)(`span`,{className:`text-[0.8125rem] text-ink-3 tabular-nums`,children:C(e)}),(0,O.jsx)(`span`,{className:`ml-auto text-[0.8125rem] text-ink-4`,children:L(e.commits.length)})]}),(0,O.jsx)(`div`,{className:`mt-2 pl-6`,children:(0,O.jsx)(x,{release:e})})]},e.version??`unreleased`))}):null,D===`credits`?(0,O.jsx)(P,{active:!0}):null]},D)]})}export{R as AboutDialog};