@gentbajko/slopify 2.0.1 → 2.0.3

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 (162) hide show
  1. package/dist/adapters/alignment/cache.js +17 -1
  2. package/dist/adapters/alignment/prefetch.js +35 -0
  3. package/dist/adapters/alignment/protocol.js +11 -1
  4. package/dist/adapters/alignment/runner.js +4 -1
  5. package/dist/adapters/alignment/worker.js +28 -5
  6. package/dist/adapters/explain.js +93 -0
  7. package/dist/adapters/ffmpeg.js +3 -3
  8. package/dist/adapters/host-cli/index.js +9 -2
  9. package/dist/adapters/host-cli/transport.js +4 -4
  10. package/dist/adapters/image/bytes.js +2 -2
  11. package/dist/adapters/image/codex-output.js +12 -12
  12. package/dist/adapters/image/codex.js +24 -8
  13. package/dist/adapters/image/fal.js +12 -6
  14. package/dist/adapters/image/google.js +18 -8
  15. package/dist/adapters/image/models.js +8 -2
  16. package/dist/adapters/image/openai.js +15 -6
  17. package/dist/adapters/image/replicate.js +16 -7
  18. package/dist/adapters/llm/claude-code-models.js +1 -1
  19. package/dist/adapters/llm/claude-code.js +16 -7
  20. package/dist/adapters/llm/cli-login-error.js +2 -1
  21. package/dist/adapters/llm/codex-models.js +1 -1
  22. package/dist/adapters/llm/codex.js +5 -6
  23. package/dist/adapters/llm/document-workspace.js +2 -2
  24. package/dist/adapters/llm/gemini-models.js +1 -1
  25. package/dist/adapters/llm/gemini-workspace.js +1 -1
  26. package/dist/adapters/llm/gemini.js +15 -11
  27. package/dist/adapters/llm/openrouter.js +21 -9
  28. package/dist/adapters/llm/run-cli.js +14 -11
  29. package/dist/adapters/tts/cartesia.js +10 -3
  30. package/dist/adapters/tts/elevenlabs.js +18 -6
  31. package/dist/adapters/tts/inworld-async.js +47 -16
  32. package/dist/adapters/tts/inworld.js +25 -15
  33. package/dist/adapters/tts/openai.js +18 -6
  34. package/dist/catalog/registry.js +13 -10
  35. package/dist/catalog/store.js +1 -1
  36. package/dist/catalog/validate.js +13 -4
  37. package/dist/edge/cli.js +37 -8
  38. package/dist/edge/docker-launch.js +6 -6
  39. package/dist/edge/docker-projects/engine.js +34 -13
  40. package/dist/edge/docker-projects/install.js +8 -8
  41. package/dist/edge/docker-projects/state.js +6 -6
  42. package/dist/edge/docker-projects/tree.js +6 -6
  43. package/dist/edge/docker.js +13 -13
  44. package/dist/edge/http/actions.js +27 -25
  45. package/dist/edge/http/app.js +3 -3
  46. package/dist/edge/http/audio-preview.js +1 -1
  47. package/dist/edge/http/checkpoints.js +5 -1
  48. package/dist/edge/http/diagnostics.js +3 -1
  49. package/dist/edge/http/draft-files.js +2 -2
  50. package/dist/edge/http/draft-problem.js +11 -1
  51. package/dist/edge/http/files.js +4 -4
  52. package/dist/edge/http/folder-location.js +3 -3
  53. package/dist/edge/http/fonts.js +22 -10
  54. package/dist/edge/http/host-cli.js +34 -12
  55. package/dist/edge/http/multipart.js +7 -5
  56. package/dist/edge/http/open-folder.js +2 -2
  57. package/dist/edge/http/planning.js +4 -4
  58. package/dist/edge/http/problem.js +2 -2
  59. package/dist/edge/http/project-templates.js +7 -3
  60. package/dist/edge/http/projects.js +8 -8
  61. package/dist/edge/http/prompts.js +9 -4
  62. package/dist/edge/http/providers.js +15 -7
  63. package/dist/edge/http/revision-files.js +3 -3
  64. package/dist/edge/http/revisions.js +30 -4
  65. package/dist/edge/http/schedules.js +9 -9
  66. package/dist/edge/http/settings.js +15 -9
  67. package/dist/edge/http/staging.js +5 -5
  68. package/dist/edge/http/storage.js +4 -4
  69. package/dist/edge/http/subtitles.js +1 -1
  70. package/dist/edge/http/tutorial.js +2 -2
  71. package/dist/edge/http/update.js +3 -3
  72. package/dist/host-cli/install.js +28 -6
  73. package/dist/host-cli/paths.js +6 -6
  74. package/dist/host-cli/server.js +1 -1
  75. package/dist/host-cli/service.js +11 -11
  76. package/dist/host-cli/status.js +7 -3
  77. package/dist/kernel/config/index.js +6 -4
  78. package/dist/kernel/db/migrate.js +2 -1
  79. package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
  80. package/dist/kernel/errors.js +12 -0
  81. package/dist/kernel/lock.js +4 -3
  82. package/dist/kernel/paths.js +5 -0
  83. package/dist/kernel/ports/subtitles.js +15 -1
  84. package/dist/kernel/runner/attempt.js +26 -2
  85. package/dist/kernel/runner/index.js +16 -6
  86. package/dist/kernel/runner/progress.js +56 -0
  87. package/dist/main.js +20 -4
  88. package/dist/slices/admission/rules.js +32 -20
  89. package/dist/slices/article/continuation.js +4 -2
  90. package/dist/slices/article/plain.js +2 -2
  91. package/dist/slices/article/provided-entries.js +3 -3
  92. package/dist/slices/article/run.js +2 -2
  93. package/dist/slices/article/segments.js +5 -3
  94. package/dist/slices/control/providers.js +20 -7
  95. package/dist/slices/fonts/catalog.js +1 -1
  96. package/dist/slices/fonts/upload.js +1 -1
  97. package/dist/slices/images/run.js +5 -5
  98. package/dist/slices/library/lint.js +13 -7
  99. package/dist/slices/library/slots.js +2 -2
  100. package/dist/slices/narration/concat.js +1 -1
  101. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  102. package/dist/slices/narration/pronunciation.js +2 -2
  103. package/dist/slices/narration/run.js +7 -7
  104. package/dist/slices/narration/steering.js +3 -3
  105. package/dist/slices/play-drafts/convert.js +5 -4
  106. package/dist/slices/play-drafts/readiness.js +12 -6
  107. package/dist/slices/play-drafts/review-inputs.js +7 -4
  108. package/dist/slices/play-drafts/review.js +3 -3
  109. package/dist/slices/play-drafts/service.js +3 -1
  110. package/dist/slices/play-drafts/uploads.js +1 -1
  111. package/dist/slices/rebuild/admission-repo.js +2 -2
  112. package/dist/slices/rebuild/recipe-legacy.js +1 -1
  113. package/dist/slices/rebuild/recipe-narration-text.js +1 -1
  114. package/dist/slices/rebuild/recipe-visual.js +1 -1
  115. package/dist/slices/rebuild/recovery.js +2 -2
  116. package/dist/slices/rebuild/repo.js +3 -3
  117. package/dist/slices/rebuild/runtime-admission.js +4 -4
  118. package/dist/slices/rebuild/runtime-article.js +5 -5
  119. package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
  120. package/dist/slices/rebuild/runtime-export-inputs.js +6 -6
  121. package/dist/slices/rebuild/runtime-export.js +4 -4
  122. package/dist/slices/rebuild/runtime-local.js +6 -6
  123. package/dist/slices/rebuild/runtime-narration-text.js +46 -7
  124. package/dist/slices/rebuild/runtime-piece-label.js +62 -0
  125. package/dist/slices/rebuild/runtime-provider.js +6 -4
  126. package/dist/slices/rebuild/runtime-publication.js +3 -3
  127. package/dist/slices/rebuild/runtime-run.js +11 -1
  128. package/dist/slices/rebuild/runtime-store.js +45 -15
  129. package/dist/slices/rebuild/runtime-subtitles.js +80 -17
  130. package/dist/slices/rebuild/transition-repo.js +6 -6
  131. package/dist/slices/reruns/index.js +1 -1
  132. package/dist/slices/research/run.js +9 -7
  133. package/dist/slices/research/synthesis.js +4 -2
  134. package/dist/slices/revisions/mutation-assets.js +26 -8
  135. package/dist/slices/revisions/mutation-cues.js +1 -1
  136. package/dist/slices/revisions/rules.js +18 -9
  137. package/dist/slices/revisions/schema.js +1 -1
  138. package/dist/slices/schedules/repo.js +55 -11
  139. package/dist/slices/schedules/scheduler.js +25 -8
  140. package/dist/slices/schedules/schema.js +19 -4
  141. package/dist/slices/schedules/service.js +2 -0
  142. package/dist/slices/settings/cli-paths.js +7 -4
  143. package/dist/slices/settings/models.js +4 -4
  144. package/dist/slices/settings/playback.js +2 -2
  145. package/dist/slices/storage/assets.js +5 -5
  146. package/dist/slices/storage/layout.js +1 -1
  147. package/dist/slices/subtitles/captions.js +1 -1
  148. package/dist/slices/subtitles/prepare.js +7 -6
  149. package/dist/slices/subtitles/transcript.js +1 -1
  150. package/dist/slices/thumbnail/run.js +8 -8
  151. package/dist/slices/video/audio-inputs.js +2 -2
  152. package/dist/slices/video/ffmpeg.js +6 -4
  153. package/dist/slices/video/plan.js +1 -1
  154. package/dist/slices/video/run.js +2 -2
  155. package/dist/updater/plan.js +1 -1
  156. package/dist/updater/service.js +4 -3
  157. package/dist/updater/worker.js +6 -4
  158. package/dist/web/assets/{index-CZNiHm8P.css → index-Bwtmu9ei.css} +1 -1
  159. package/dist/web/assets/index-DjhMJqMN.js +141 -0
  160. package/dist/web/index.html +2 -2
  161. package/package.json +1 -1
  162. package/dist/web/assets/index-DPgGLX5m.js +0 -137
package/dist/edge/cli.js CHANGED
@@ -23,17 +23,18 @@ const { values } = parseArgs({
23
23
  "accept-host-cli": { type: "boolean" },
24
24
  },
25
25
  });
26
+ let config;
26
27
  try {
27
28
  if (values["projects-dir"] !== undefined && !values.docker)
28
- throw new Error("--projects-dir requires --docker; native installs use --data-dir.");
29
+ throw new Error("--projects-dir only works together with --docker. Without Docker, choose where Slopify keeps its files with --data-dir <folder>.");
29
30
  if ((values["host-cli"] !== undefined || values["accept-host-cli"] !== undefined) &&
30
31
  !values.docker)
31
- throw new Error("Host CLI options require --docker.");
32
+ throw new Error("--host-cli and --accept-host-cli only work together with --docker. Add --docker, or remove those options.");
32
33
  if (values["host-cli"] !== undefined && values["host-cli"] !== "off")
33
- throw new Error("The --host-cli override supports only off (API-only Docker).");
34
+ throw new Error(`--host-cli=${values["host-cli"]} is not supported. The only value is --host-cli=off, which starts Docker using API keys only; leave the option out to use the AI CLIs installed on this machine.`);
34
35
  if (values.docker) {
35
36
  if (values.host !== undefined || values["data-dir"] !== undefined)
36
- throw new Error("Docker keeps private data in its named volume and binds to localhost. Use --projects-dir for project files and --port for the port.");
37
+ throw new Error("--host and --data-dir can't be used with --docker: the Docker version always listens on localhost and keeps its data in a Docker volume. Use --projects-dir <folder> for project files and --port <number> for the port.");
37
38
  const docker = await import("./docker.js");
38
39
  docker.assertManagedDockerHost(process.platform, process.getuid?.(), process.getgid?.());
39
40
  const { prepareDockerHostCli } = docker;
@@ -73,14 +74,17 @@ try {
73
74
  },
74
75
  });
75
76
  if (result.error)
76
- throw result.error;
77
+ throw new Error(`Could not start the Docker launcher script (${result.error.message}). Make sure bash is installed and on your PATH, then try again.`);
77
78
  process.exit(result.status ?? 1);
78
79
  }
79
- const config = configFrom(values, process.env);
80
+ config = configFrom(values, process.env);
80
81
  const forwarded = await forwardManagedUpdate(config.dataDir, readVersion(), process.argv.slice(2));
81
82
  if (forwarded !== undefined)
82
83
  process.exit(forwarded);
83
- const { paths, url, stop } = await boot(config);
84
+ const { paths, url, stop } = await boot(config, {
85
+ prefetchSubtitleModel: ["", "0", "false"].includes((process.env.SLOPIFY_NO_MODEL_PREFETCH ?? "").trim().toLowerCase()),
86
+ subtitleModelSeed: process.env.SLOPIFY_SUBTITLE_MODEL_SEED?.trim() || undefined,
87
+ });
84
88
  console.log(`Slopify is running at ${url}`);
85
89
  console.log(`Slopify data directory: ${paths.dataDir}`);
86
90
  console.log(`Database: ${paths.db}`);
@@ -100,6 +104,31 @@ try {
100
104
  });
101
105
  }
102
106
  catch (error) {
103
- console.error(error instanceof Error ? error.message : String(error));
107
+ console.error(explainStartupError(error, config));
104
108
  process.exit(1);
105
109
  }
110
+ // Node's own wording for a failed listen or file operation ("listen EADDRINUSE: address already
111
+ // in use") names the symptom but not the fix, so the common startup failures are restated here.
112
+ function explainStartupError(error, config) {
113
+ if (!(error instanceof Error))
114
+ return String(error);
115
+ const code = "code" in error && typeof error.code === "string" ? error.code : undefined;
116
+ const syscall = "syscall" in error && typeof error.syscall === "string" ? error.syscall : "";
117
+ if (config !== undefined && syscall === "listen") {
118
+ const other = config.port === 7070 ? 7071 : 7070;
119
+ if (code === "EADDRINUSE")
120
+ return `Port ${config.port} is already in use by another program (maybe another Slopify). Stop that program, or start Slopify on another port: npx @gentbajko/slopify --port ${other}`;
121
+ if (code === "EACCES" || code === "EPERM")
122
+ return `Slopify is not allowed to use port ${config.port}${config.port < 1024 ? " (ports below 1024 need administrator rights)" : ""}. Start it on another port: npx @gentbajko/slopify --port ${other}`;
123
+ if (code === "EADDRNOTAVAIL" || code === "ENOTFOUND" || code === "EAI_AGAIN")
124
+ return `Slopify can't listen on ${config.host} because that address doesn't belong to this machine. Leave out --host (and SLOPIFY_HOST) to use 127.0.0.1, or use an address this machine owns.`;
125
+ }
126
+ const path = "path" in error && typeof error.path === "string" ? error.path : undefined;
127
+ if (config !== undefined &&
128
+ path?.startsWith(config.dataDir) &&
129
+ (code === "EACCES" || code === "EPERM" || code === "EROFS"))
130
+ return `Slopify can't write to its data folder ${config.dataDir} (${error.message}). Make sure your user owns that folder and can write to it, or choose another one: npx @gentbajko/slopify --data-dir <folder>`;
131
+ if (code === "ENOSPC")
132
+ return `The disk is full (${error.message}). Free up some space and start Slopify again.`;
133
+ return error.message;
134
+ }
@@ -15,7 +15,7 @@ async function main() {
15
15
  const gid = process.getgid?.();
16
16
  assertManagedDockerHost(process.platform, uid, gid);
17
17
  if (gid === undefined)
18
- throw new Error("Missing host group.");
18
+ throw new Error("Could not read your user's group. Run the --docker launcher as your normal logged-in user.");
19
19
  const c = dockerConfig(process.env, homedir(), process.cwd(), uid, gid);
20
20
  await privateDirectory(c.root, uid);
21
21
  if (process.argv[2] !== "--locked") {
@@ -24,7 +24,7 @@ async function main() {
24
24
  try {
25
25
  const s = await fd.stat();
26
26
  if (!s.isFile() || s.nlink !== 1 || s.uid !== uid || (s.mode & 0o077) !== 0)
27
- throw new Error("Unsafe Docker setup lock.");
27
+ throw new Error(`The launcher's lock file ${path} has the wrong owner or permissions. Delete it and run the launcher again.`);
28
28
  const child = spawn("flock", [
29
29
  "--nonblock",
30
30
  "--no-fork",
@@ -36,13 +36,13 @@ async function main() {
36
36
  "--locked",
37
37
  ], { stdio: "inherit" });
38
38
  const code = await new Promise((resolve, reject) => {
39
- child.once("error", () => reject(new Error("Install util-linux (flock) before using the managed Docker launcher.")));
39
+ child.once("error", () => reject(new Error("The --docker launcher needs the flock command, which isn't installed. Install util-linux (for example sudo apt install util-linux, or sudo pacman -S util-linux) and try again.")));
40
40
  child.once("exit", (status, signal) => signal
41
- ? reject(new Error("Docker setup was interrupted; rerun to recover."))
41
+ ? reject(new Error("Docker setup was interrupted before it finished. Run the same command again; Slopify will finish or undo the half-done step."))
42
42
  : resolve(status ?? 1));
43
43
  });
44
44
  if (code === 73)
45
- throw new Error("Another managed Docker installation is changing storage. Wait for it to finish and rerun.");
45
+ throw new Error("Another Slopify --docker launcher is already running and changing your installation. Wait for it to finish, then run the command again.");
46
46
  process.exitCode = code;
47
47
  return;
48
48
  }
@@ -51,7 +51,7 @@ async function main() {
51
51
  }
52
52
  }
53
53
  const controller = new AbortController();
54
- const abort = () => controller.abort(new Error("Docker setup interrupted."));
54
+ const abort = () => controller.abort(new Error("Docker setup was stopped before it finished. Run the same command again; Slopify will finish or undo the half-done step."));
55
55
  process.once("SIGINT", abort);
56
56
  process.once("SIGTERM", abort);
57
57
  try {
@@ -4,12 +4,27 @@ import { setTimeout as delay } from "node:timers/promises";
4
4
  import { z } from "zod";
5
5
  import { containerSchema, digestSchema, } from "./state.js";
6
6
  import { contains } from "./tree.js";
7
+ // Docker's exit code alone says nothing, so its last error line decides the advice: the daemon
8
+ // being stopped and the socket being off-limits are by far the most common first-run failures.
9
+ export function dockerFailure(args, result) {
10
+ const detail = (result.stderr ?? "").trim();
11
+ if (/permission denied[^\n]*docker\.sock|docker\.sock[^\n]*permission denied/i.test(detail))
12
+ return "Your user isn't allowed to use Docker. Give it access with sudo usermod -aG docker $USER, then log out and back in (or run newgrp docker) and try again.";
13
+ if (/cannot connect to the docker daemon|is the docker daemon running|error during connect/i.test(detail))
14
+ return "Docker is installed but not running. Start it (sudo systemctl start docker, or systemctl --user start docker for rootless Docker) and try again.";
15
+ if (/port is already allocated|address already in use/i.test(detail))
16
+ return "The port Slopify wants is already in use by another program. Stop that program, or start Slopify on another port: npx @gentbajko/slopify --docker --port 7070";
17
+ if (/no space left on device/i.test(detail))
18
+ return "The disk Docker uses is full. Free up space (for example docker system prune) and try again.";
19
+ const last = detail.split("\n").at(-1)?.trim().slice(0, 300);
20
+ return `Docker command "docker ${args[0] ?? ""}" failed (exit code ${result.code}${last ? `: ${last}` : ""}). Check that Docker works (docker info) and run the launcher again. Nothing was deleted, and any backup Slopify made is kept.`;
21
+ }
7
22
  export function dockerEngine(runner, signal, env) {
8
23
  const run = (args, localSignal = signal) => runner.exec("docker", args, localSignal);
9
24
  async function command(args, localSignal = signal) {
10
25
  const r = await run(args, localSignal);
11
26
  if (r.code !== 0)
12
- throw new Error(`Docker ${args[0] ?? "command"} failed; recovery material is retained.`);
27
+ throw new Error(dockerFailure(args, r));
13
28
  return r.stdout.trim();
14
29
  }
15
30
  const mount = (type, source, target, readonly = false) => [
@@ -48,14 +63,14 @@ export function dockerEngine(runner, signal, env) {
48
63
  "{{.ID}}",
49
64
  ]);
50
65
  if (present.code !== 0)
51
- throw new Error("Docker container inventory failed.");
66
+ throw new Error(dockerFailure(["container", "ls"], present));
52
67
  if (!present.stdout.trim() && !/^[a-f0-9]{12,64}$/.test(name))
53
68
  return null;
54
69
  const result = await run(["inspect", name]);
55
70
  if (result.code !== 0) {
56
71
  if (!present.stdout.trim())
57
72
  return null;
58
- throw new Error("Docker inspect failed.");
73
+ throw new Error(dockerFailure(["inspect", name], result));
59
74
  }
60
75
  const schema = z.array(z.object({
61
76
  Id: z.string(),
@@ -105,11 +120,11 @@ export function dockerEngine(runner, signal, env) {
105
120
  for (const id of ids) {
106
121
  const c = await inspect(id);
107
122
  if (!c)
108
- throw new Error("Cannot verify volume claims; an inventoried container is missing.");
123
+ throw new Error("Cannot verify volume claims: a Docker container disappeared while Slopify was checking. Run the launcher again.");
109
124
  if (!permitted.includes(c.id) &&
110
125
  !spared(c) &&
111
126
  c.mounts.some((m) => m.type === "volume" && m.name === volume))
112
- throw new Error(`Another container claims installation volume ${volume}: ${c.name}.`);
127
+ throw new Error(`The Docker volume ${volume} that holds Slopify's data is also used by the container ${c.name}. Remove that container if you no longer need it (docker rm ${c.name}), or give this Slopify its own volume with SLOPIFY_DOCKER_VOLUME=<name>, then try again.`);
113
128
  }
114
129
  }
115
130
  async function writers(volume, paths, permitted) {
@@ -122,7 +137,7 @@ export function dockerEngine(runner, signal, env) {
122
137
  (m.name === volume ||
123
138
  (m.type === "bind" &&
124
139
  paths.some((p) => contains(p, m.source) || contains(m.source, p))))))
125
- throw new Error(`Another running container writes installation storage: ${c.name}. Stop it before retrying.`);
140
+ throw new Error(`Another running container (${c.name}) is writing to Slopify's data or project folder. Stop it (docker stop ${c.name}) and try again.`);
126
141
  }
127
142
  }
128
143
  async function recoveryLabel(j) {
@@ -154,7 +169,13 @@ export function dockerEngine(runner, signal, env) {
154
169
  .parse(raw)[0]?.Endpoints.docker.Host;
155
170
  }
156
171
  if (!endpoint?.startsWith("unix://"))
157
- throw new Error("Managed folders require a local Linux Docker unix socket; remote daemons are unsupported.");
172
+ throw new Error(`The --docker launcher only works with Docker running on this machine, not a remote Docker (${endpoint ?? "unknown endpoint"}). Switch to the local one (unset DOCKER_HOST, or docker context use default) and try again.`);
173
+ const rawInfo = JSON.parse(await command(["info", "--format", "{{json .}}"]));
174
+ // Some Docker versions print the daemon's absence inside the JSON instead of failing.
175
+ const serverErrors = z.object({ ServerErrors: z.array(z.string()) }).safeParse(rawInfo)
176
+ .data?.ServerErrors;
177
+ if (serverErrors !== undefined && serverErrors.length > 0)
178
+ throw new Error(dockerFailure(["info"], { code: 1, stderr: serverErrors.join("\n") }));
158
179
  const info = z
159
180
  .object({
160
181
  ID: z.string(),
@@ -162,12 +183,12 @@ export function dockerEngine(runner, signal, env) {
162
183
  OperatingSystem: z.string(),
163
184
  SecurityOptions: z.array(z.string()),
164
185
  })
165
- .parse(JSON.parse(await command(["info", "--format", "{{json .}}"])));
186
+ .parse(rawInfo);
166
187
  if (/docker desktop/i.test(info.OperatingSystem))
167
- throw new Error("Docker Desktop is unsupported by managed project folders. Use a native Linux Docker daemon or native Slopify.");
188
+ throw new Error("Docker Desktop isn't supported by the --docker launcher because it can't give your project folder the right owner. Use Docker Engine on Linux (docker context use default), or run Slopify without Docker: npx @gentbajko/slopify");
168
189
  const rootless = info.SecurityOptions.includes("name=rootless");
169
190
  if (!rootless && info.SecurityOptions.some((s) => s.startsWith("name=userns")))
170
- throw new Error("Docker userns-remap is unsupported by managed project folders. Use a supported rootless daemon or native Slopify; do not disable isolation.");
191
+ throw new Error("Your Docker uses userns-remap, which the --docker launcher doesn't support. Use rootless Docker or a standard Docker Engine setup, or run Slopify without Docker: npx @gentbajko/slopify. Don't turn off Docker's isolation to get around this.");
171
192
  return { daemon: info.ID, user: rootless ? "0:0" : `${uid}:${gid}` };
172
193
  },
173
194
  image: async (ref) => {
@@ -214,7 +235,7 @@ export function dockerEngine(runner, signal, env) {
214
235
  })
215
236
  .parse(JSON.parse(await command(["volume", "inspect", "--format", "{{json .}}", name])));
216
237
  if (v.Name !== name || (v.Options && Object.keys(v.Options).length > 0))
217
- throw new Error("Managed projects require an ordinary local named volume; remote/custom volume drivers are unsupported.");
238
+ throw new Error(`The Docker volume ${name} uses a custom driver or options, which Slopify doesn't support. Give Slopify a plain local volume with SLOPIFY_DOCKER_VOLUME=<new name> and try again.`);
218
239
  return JSON.stringify([v.Name, v.CreatedAt, v.Driver]);
219
240
  },
220
241
  probe: async (c, image, user, destination) => {
@@ -228,7 +249,7 @@ export function dockerEngine(runner, signal, env) {
228
249
  s.gid !== c.gid ||
229
250
  (s.mode & 0o077) !== 0 ||
230
251
  (await readFile(join(probe, "container"), "utf8")) !== "slopify ownership probe")
231
- throw new Error("Docker cannot create private files as this host user; ownership mapping is unsupported.");
252
+ throw new Error("Docker can't create files in your project folder as your user, so the files would end up with the wrong owner. Use a standard Docker Engine or rootless Docker setup, or run Slopify without Docker: npx @gentbajko/slopify");
232
253
  await writeFile(join(probe, "container"), "host can write", { flag: "a" });
233
254
  }
234
255
  finally {
@@ -378,7 +399,7 @@ export function dockerEngine(runner, signal, env) {
378
399
  }
379
400
  await delay(Math.min(250, Math.max(0, deadline - Date.now())), undefined, { signal });
380
401
  }
381
- throw new Error("Replacement did not become healthy within 120 seconds.");
402
+ throw new Error(`The new Slopify container did not start up within 2 minutes. See why with: docker logs ${id}`);
382
403
  },
383
404
  };
384
405
  }
@@ -17,10 +17,10 @@ export async function installProjects(c, e, recovery) {
17
17
  const currentVolume = await e.volume(c.volume);
18
18
  if ((receipt && receipt.volumeIdentity !== currentVolume) ||
19
19
  (previousJournal?.volumeIdentity && previousJournal.volumeIdentity !== currentVolume))
20
- throw new Error("Remembered data volume is missing or was replaced; restore the original volume before retrying.");
20
+ throw new Error(`Remembered data volume ${c.volume} is missing or was replaced since Slopify was installed. Restore the original volume (see docker volume ls) and run the launcher again. Nothing was changed.`);
21
21
  if ((receipt && receipt.daemon !== context.daemon) ||
22
22
  (previousJournal && previousJournal.daemon !== context.daemon))
23
- throw new Error("Docker daemon differs from saved installation; select the original daemon.");
23
+ throw new Error("This is not the Docker that Slopify was installed with (a different Docker context or DOCKER_HOST is active). Switch back to the original one (docker context use <name>, or unset DOCKER_HOST) and try again.");
24
24
  await assertVolumeClaims(c, e, context.daemon, currentVolume, await e.inspect(c.name));
25
25
  if (previousJournal && previousJournal.phase !== "rolled-back")
26
26
  previousJournal = await recoverInstallation(c, previousJournal, receipt, recovery());
@@ -42,7 +42,7 @@ export async function installProjects(c, e, recovery) {
42
42
  continue;
43
43
  const other = await readState(join(c.root, name, "receipt.json"), receiptSchema, c.uid);
44
44
  if (other?.daemon === context.daemon && other.volume === c.volume)
45
- throw new Error(`Named volume belongs to installation ${other.name}; select a separate volume.`);
45
+ throw new Error(`The Docker volume ${c.volume} already belongs to another Slopify installation (${other.name}). Give this one its own volume with SLOPIFY_DOCKER_VOLUME=<name> and try again.`);
46
46
  }
47
47
  let reuse = null;
48
48
  if (previousJournal?.phase === "rolled-back" &&
@@ -66,7 +66,7 @@ export async function installProjects(c, e, recovery) {
66
66
  const image = await e.image(c.image);
67
67
  const expectedVersion = await e.version(image);
68
68
  if (expectedVersion !== readVersion())
69
- throw new Error("Docker image and installed launcher versions differ. Refresh the default release image or select the matching custom image before retrying; the existing container is unchanged.");
69
+ throw new Error(`The Docker image is Slopify ${expectedVersion} but this launcher is ${readVersion()}. Run the latest of both (npx @gentbajko/slopify@latest --docker), or if you set SLOPIFY_DOCKER_IMAGE, point it at version ${readVersion()}. Your existing container was not changed.`);
70
70
  const signature = createHash("sha256")
71
71
  .update(JSON.stringify([image, c.volume, c.port, c.bridge, projects, context.user]))
72
72
  .digest("hex");
@@ -228,19 +228,19 @@ export async function installProjects(c, e, recovery) {
228
228
  await save({ phase: "committed" });
229
229
  const ready = await e.inspect(candidate);
230
230
  if (!ready?.port)
231
- throw new Error("Committed container has no localhost port; rerun the launcher.");
231
+ throw new Error("Slopify started but Docker did not report which port it is on. Run the same command again to finish.");
232
232
  return { url: `http://${ready.port}`, projects, recovery: j.backup };
233
233
  }
234
234
  catch (cause) {
235
235
  const current = await readState(receiptPath, receiptSchema, c.uid);
236
236
  if (current?.transaction === j.id)
237
- throw new Error(`Installation committed at ${projects}; rerun the launcher to finish activation. Recovery volume: ${j.backup}`, { cause });
237
+ throw new Error(`Slopify was installed (project files at ${projects}) but the last step did not finish. Run the same command again to complete it. Backup volume: ${j.backup}`, { cause });
238
238
  try {
239
239
  await recoverInstallation(c, j, current, recovery());
240
240
  }
241
241
  catch (failure) {
242
- throw new Error(`Recovery incomplete. Leave container/data stopped; recovery volume ${j.backup}, journal ${journalPath}, project folder ${projects}. ${failure instanceof Error ? failure.message : "Recovery operation failed."}`, { cause });
242
+ throw new Error(`Recovery incomplete: the install failed and Slopify could not fully undo it. Leave the Slopify container stopped and don't delete anything: backup volume ${j.backup}, progress file ${journalPath}, project folder ${projects}. Fix the problem below, then run the same command again. ${failure instanceof Error ? failure.message : "The undo step failed."}`, { cause });
243
243
  }
244
- throw new Error(`Previous installation restored${old?.running ? " and restarted" : " without starting it"}. Recovery: ${j.backup}; project copy: ${projects}. ${cause instanceof Error ? cause.message : "Installation failed."}`, { cause });
244
+ throw new Error(`Install failed. Previous installation restored${old?.running ? " and restarted" : " (not started)"}, so nothing was lost. Backup volume: ${j.backup}; project copy: ${projects}. Reason: ${cause instanceof Error ? cause.message : "Installation failed."}`, { cause });
245
245
  }
246
246
  }
@@ -112,7 +112,7 @@ export function dockerConfig(env, home, cwd, uid, gid) {
112
112
  const projectsOverride = raw === null ? null : resolve(cwd, raw.startsWith("~/") ? join(home, raw.slice(2)) : raw);
113
113
  const portText = env.SLOPIFY_DOCKER_HOST_PORT || "6969";
114
114
  if (!/^\d{1,5}$/.test(portText) || Number(portText) > 65535)
115
- throw new Error("Docker host port must be from 0 to 65535.");
115
+ throw new Error(`Invalid port ${JSON.stringify(portText)}: use a whole number between 1 and 65535, for example --port 7070.`);
116
116
  return {
117
117
  home,
118
118
  uid,
@@ -144,11 +144,11 @@ export async function privateDirectory(path, uid) {
144
144
  if (!s.isDirectory() ||
145
145
  s.isSymbolicLink() ||
146
146
  ((s.mode & 0o022) !== 0 && (s.mode & 0o1000) === 0))
147
- throw new Error("Unsafe private state ancestor.");
147
+ throw new Error(`${current} is not a normal folder, or other users can write to it, so Slopify won't keep its Docker settings under it. Fix its permissions (chmod go-w ${current}) or set XDG_DATA_HOME to another folder, then try again.`);
148
148
  }
149
149
  const s = await lstat(path);
150
150
  if (s.uid !== uid || (s.mode & 0o077) !== 0)
151
- throw new Error("Installation state must be private and owned by the installing user.");
151
+ throw new Error(`${path} must belong to you and be private. Fix it with chmod 700 ${path} (and chown it to your user if needed), then try again.`);
152
152
  }
153
153
  export async function readState(path, schema, uid) {
154
154
  const s = await lstat(path).catch((error) => {
@@ -186,7 +186,7 @@ export async function selectProjects(c, receipt, old, retry = null) {
186
186
  throw new Error("Receipt selects a different installation or volume.");
187
187
  await safePath(receipt.projects, c.home, c.root, false);
188
188
  await assertIdentity(receipt.projects, receipt.directoryIdentity).catch((cause) => {
189
- throw new Error(`Remembered project folder is missing or replaced: ${receipt.projects}`, {
189
+ throw new Error(`Remembered project folder ${receipt.projects} is missing or was replaced. Put the original folder back where it was and run the launcher again.`, {
190
190
  cause,
191
191
  });
192
192
  });
@@ -202,7 +202,7 @@ export async function selectProjects(c, receipt, old, retry = null) {
202
202
  await safePath(chosen, c.home, c.root, true);
203
203
  const source = receipt?.projects ?? bind?.source;
204
204
  if (source && source !== chosen && (contains(source, chosen) || contains(chosen, source)))
205
- throw new Error("Source and destination project folders cannot contain each other.");
205
+ throw new Error(`The new project folder ${chosen} and the current one ${source} are inside each other. Choose a separate folder with --projects-dir <folder>.`);
206
206
  if (chosen !== source) {
207
207
  const entries = await readdir(chosen).catch((error) => {
208
208
  if (isMissing(error))
@@ -211,7 +211,7 @@ export async function selectProjects(c, receipt, old, retry = null) {
211
211
  });
212
212
  if (entries.length > 0) {
213
213
  if (retry?.destination !== chosen || !retry.publishedIdentity || !retry.sourceDigest)
214
- throw new Error(`Unrelated populated destination: ${chosen}`);
214
+ throw new Error(`The project folder ${chosen} already has files in it that Slopify didn't put there. Choose an empty or new folder with --projects-dir <folder>, or move those files away, then try again.`);
215
215
  await assertIdentity(chosen, retry.publishedIdentity);
216
216
  if ((await treeDigest(chosen)).hash !== retry.sourceDigest.hash)
217
217
  throw new Error(`Published retry destination changed: ${chosen}`);
@@ -19,7 +19,7 @@ export function contains(root, path) {
19
19
  }
20
20
  export async function safePath(path, home, stateRoot, createParents) {
21
21
  if (!isAbsolute(path) || resolve(path) !== path || /[\p{Cc},]/u.test(path))
22
- throw new Error("Project directory must be a normal absolute path without commas/control characters.");
22
+ throw new Error(`The project folder ${JSON.stringify(path)} can't be used: give a full path without commas, for example --projects-dir ~/Slopify/Projects.`);
23
23
  const forbidden = [
24
24
  "/",
25
25
  "/home",
@@ -36,7 +36,7 @@ export async function safePath(path, home, stateRoot, createParents) {
36
36
  contains(stateRoot, path) ||
37
37
  contains(path, stateRoot) ||
38
38
  ["/proc", "/sys", "/dev", "/etc", "/usr", "/bin", "/sbin", "/lib", "/var/lib/docker"].some((p) => contains(p, path)))
39
- throw new Error("Unsafe broad or private project directory.");
39
+ throw new Error(`${path} can't be the project folder because it is a system or shared folder (or holds Slopify's own settings). Choose a dedicated folder, for example --projects-dir ~/Slopify/Projects.`);
40
40
  const parts = path.split(sep).filter(Boolean);
41
41
  let current = sep;
42
42
  for (const [i, part] of parts.entries()) {
@@ -53,11 +53,11 @@ export async function safePath(path, home, stateRoot, createParents) {
53
53
  if (s === undefined)
54
54
  continue;
55
55
  if (!s.isDirectory() || s.isSymbolicLink())
56
- throw new Error(`Unsafe path component: ${current}`);
56
+ throw new Error(`${current} is a file or a link, not a folder, so it can't be part of the project folder path. Choose another folder with --projects-dir <folder>.`);
57
57
  if (i === parts.length - 1 && (s.uid !== process.getuid?.() || (s.mode & 0o022) !== 0))
58
- throw new Error("Project directory must belong to the installing user and must not be publicly writable.");
58
+ throw new Error(`The project folder ${path} must belong to you and must not be writable by other users. Fix it (chmod go-w ${path}) or choose another folder with --projects-dir <folder>.`);
59
59
  if (i < parts.length - 1 && (s.mode & 0o022) !== 0 && (s.mode & 0o1000) === 0)
60
- throw new Error(`Writable project ancestor: ${current}`);
60
+ throw new Error(`Other users can write to ${current}, which contains the project folder. Choose a project folder inside your home folder with --projects-dir <folder>, or fix it with chmod go-w ${current}.`);
61
61
  }
62
62
  }
63
63
  export function isMissing(error) {
@@ -137,7 +137,7 @@ export async function assertWritableTree(root, uid) {
137
137
  (s.isFile() && s.nlink !== 1) ||
138
138
  s.uid !== uid ||
139
139
  (s.mode & (s.isDirectory() ? 0o700 : 0o600)) !== (s.isDirectory() ? 0o700 : 0o600))
140
- throw new Error(`Existing project permissions are incompatible with this host user: ${root}`);
140
+ throw new Error(`${root} in your project files doesn't have permissions your user can fully use. Make it yours and writable (chown -R $USER ${root}; chmod -R u+rwX ${root}), then try again.`);
141
141
  if (s.isDirectory())
142
142
  for (const name of await readdir(root))
143
143
  await assertWritableTree(join(root, name), uid);
@@ -5,13 +5,13 @@ import { z } from "zod";
5
5
  import { installHostPackage } from "../host-cli/install.js";
6
6
  import { ensureBridgeToken, hasCode, prepareHostPaths } from "../host-cli/paths.js";
7
7
  import { ensureHostService, privateRead, privateWrite } from "../host-cli/service.js";
8
- import { resolveHostCommand } from "../host-cli/status.js";
8
+ import { hostCommandName, resolveHostCommand } from "../host-cli/status.js";
9
9
  import { hostCliProtocol, hostLlmIds } from "../kernel/ports/host-cli.js";
10
10
  export function assertManagedDockerHost(platform, uid, gid) {
11
11
  if (platform !== "linux")
12
- throw new Error("The managed Docker launcher requires Linux. Native Slopify and plain API-only Docker remain available on other hosts.");
12
+ throw new Error("The --docker launcher only works on Linux. On this system, run Slopify without Docker (npx @gentbajko/slopify), or start the Slopify Docker image yourself with docker run.");
13
13
  if (uid === undefined || gid === undefined || uid === 0)
14
- throw new Error("Run the managed Docker launcher as your logged-in user, not with sudo; grant that account access to Docker.");
14
+ throw new Error("Don't run the --docker launcher with sudo or as root. Run it as your normal user, and if Docker then says permission denied, give your user access: sudo usermod -aG docker $USER, then log out and back in.");
15
15
  }
16
16
  export async function prepareDockerHostCli(options) {
17
17
  if (options.disabled)
@@ -24,16 +24,16 @@ export async function prepareDockerHostCli(options) {
24
24
  }
25
25
  catch (error) {
26
26
  if (!hasCode(error, "ENOENT"))
27
- throw new Error("Host CLI detection failed. No container was changed.");
27
+ throw new Error(`Could not check whether ${hostCommandName(id)} is installed on this machine (${error instanceof Error ? error.message : String(error)}). Nothing was changed. Fix the problem, or start with --host-cli=off to use API keys only.`);
28
28
  }
29
29
  }
30
30
  if (!installed)
31
31
  return {};
32
32
  if (process.platform !== "linux")
33
- throw new Error("Managed host CLI setup requires Linux/systemd. Use --host-cli=off for API-only Docker.");
33
+ throw new Error("Using this machine's AI CLIs from Docker needs Linux with systemd. Start with --host-cli=off to use API keys only.");
34
34
  const uid = process.getuid?.();
35
35
  if (uid === undefined || uid === 0)
36
- throw new Error("Run the Docker launcher as your logged-in user, not root; or use --host-cli=off.");
36
+ throw new Error("Don't run the --docker launcher with sudo or as root; run it as your normal user. Or start with --host-cli=off to use API keys only.");
37
37
  const receiptPath = join(options.root, "consent.json");
38
38
  const existing = await privateRead(receiptPath, uid);
39
39
  const receipt = z.object({ version: z.literal(1), automaticStartup: z.literal(true) }).strict();
@@ -43,15 +43,15 @@ export async function prepareDockerHostCli(options) {
43
43
  approved ||= receipt.safeParse(JSON.parse(existing)).success;
44
44
  }
45
45
  catch {
46
- throw new Error("Invalid host helper consent receipt.");
46
+ throw new Error(`The saved answer to the host CLI question (${receiptPath}) is damaged. Delete that file and run the launcher again to be asked again.`);
47
47
  }
48
48
  }
49
49
  if (!approved) {
50
50
  if (!options.interactive)
51
- throw new Error("Host CLI setup needs confirmation. Use --accept-host-cli to approve, or --host-cli=off for API-only Docker.");
51
+ throw new Error("Slopify needs your OK to use the Claude Code, Codex or Gemini CLI installed on this machine, but can't ask because this isn't an interactive terminal. Rerun with --accept-host-cli to allow it, or with --host-cli=off to use API keys only.");
52
52
  approved = await options.prompt("Allow Slopify to run your host Claude Code, Codex and Gemini CLIs using their existing logins? This installs a private helper and enables automatic startup. User lingering keeps your user services running after logout and at boot. Credentials stay on the host. [y/N] ");
53
53
  if (!approved)
54
- throw new Error("Host CLI setup declined. The container is unchanged. Use --host-cli=off for API-only Docker.");
54
+ throw new Error("You declined, so Slopify did not set up your host AI CLIs and nothing was changed. Run the launcher again and answer y to allow it, or add --host-cli=off to start with API keys only.");
55
55
  }
56
56
  const inspect = () => options.runner.exec("docker", [
57
57
  "image",
@@ -64,14 +64,14 @@ export async function prepareDockerHostCli(options) {
64
64
  if (image.code !== 0 || image.stdout.trim() !== String(hostCliProtocol)) {
65
65
  const pulled = await options.runner.exec("docker", ["pull", options.image], options.signal);
66
66
  if (pulled.code !== 0)
67
- throw new Error("Could not pull a compatible Docker image. No helper or container was changed.");
67
+ throw new Error(`Could not download the Slopify Docker image ${options.image}. Check your internet connection and that Docker is running (docker info), then try again. Nothing was changed.`);
68
68
  image = await inspect();
69
69
  if (image.code !== 0 || image.stdout.trim() !== String(hostCliProtocol))
70
- throw new Error("This Docker image does not support the host helper. Update the image before retrying.");
70
+ throw new Error(`The Docker image ${options.image} is too old to use the AI CLIs on this machine. Use the latest image (unset SLOPIFY_DOCKER_IMAGE, or point it to a current release) and try again, or start with --host-cli=off.`);
71
71
  }
72
72
  const supported = await options.runner.exec("systemctl", ["--user", "list-units", "--no-legend", "--no-pager", "slopify-cli-bridge.service"], options.signal);
73
73
  if (supported.code !== 0)
74
- throw new Error("The systemd user service is unavailable. Use a normal Slopify install or --host-cli=off for API-only Docker.");
74
+ throw new Error("Using this machine's AI CLIs from Docker needs systemd user services (systemctl --user), which aren't available here. Start with --host-cli=off to use API keys only, or run Slopify without Docker: npx @gentbajko/slopify.");
75
75
  const paths = await prepareHostPaths(options.root);
76
76
  const lock = join(paths.root, "setup.lock");
77
77
  const deadline = Date.now() + 30_000;
@@ -84,7 +84,7 @@ export async function prepareDockerHostCli(options) {
84
84
  if (!hasCode(error, "EEXIST"))
85
85
  throw error;
86
86
  if (Date.now() >= deadline)
87
- throw new Error("Another host helper setup holds setup.lock. Wait for it to finish; the container is unchanged.");
87
+ throw new Error(`Another Slopify launcher is setting up the host CLI helper right now. Wait for it to finish and try again. If no other launcher is running, delete the folder ${lock} and retry.`);
88
88
  await delay(100, undefined, { signal: options.signal });
89
89
  }
90
90
  }
@@ -91,15 +91,15 @@ export function actionRoutes(deps) {
91
91
  "rebuild-required": 409,
92
92
  };
93
93
  const messages = {
94
- "no-project": "No project has that id.",
95
- running: "Pause the run and wait for its active calls to stop before changing providers.",
96
- "not-editable": "Providers can be changed only while a project is paused or failed.",
97
- "invalid-providers": "The provider choices need attention.",
98
- "catalog-unavailable": "The provider model catalog could not be loaded. Try again.",
99
- "revision-required": "Reload the project before using this control. A current revision and request ID are required.",
100
- conflict: "The project changed. Reload it before using this control.",
101
- "idempotency-conflict": "This request ID has already been used for a different action.",
102
- "rebuild-required": "Review the affected outputs and cost before rebuilding this revision.",
94
+ "no-project": "This project no longer exists. Go back to Projects to pick another.",
95
+ running: "This project is still running. Pause it and wait for the current step to stop before changing providers.",
96
+ "not-editable": "Providers can be changed only while the project is paused or has failed. Pause it first, then try again.",
97
+ "invalid-providers": "Some provider choices are not valid. Fix the highlighted fields and try again.",
98
+ "catalog-unavailable": "Slopify could not load the list of available models. Wait a moment and try again; if it keeps failing, check Settings → Models.",
99
+ "revision-required": "This page is out of date. Reload the page, then try again.",
100
+ conflict: "This project changed since the page loaded. Reload the page to see the latest, then try again.",
101
+ "idempotency-conflict": "This action was already sent with different details. Reload the page, then try again.",
102
+ "rebuild-required": "This change needs some outputs to be made again. In the Edit tab, use Rebuild affected outputs to see what will be regenerated and what it costs.",
103
103
  };
104
104
  return problem(c, {
105
105
  status: codes[result.reason],
@@ -117,7 +117,7 @@ export function actionRoutes(deps) {
117
117
  return problem(c, {
118
118
  status: 503,
119
119
  title: titleOf(503),
120
- detail: "Recovery is unavailable. Try again after restart.",
120
+ detail: "Resume and retry are not ready yet because Slopify is still starting. Wait a moment, reload the page and try again.",
121
121
  });
122
122
  const result = recoveryResultSchema.parse(await recoverProject(deps.rebuild, id, { ...input, action }));
123
123
  if (result.ok)
@@ -128,26 +128,28 @@ export function actionRoutes(deps) {
128
128
  ? 400
129
129
  : 409;
130
130
  const messages = {
131
- "no-project": "This project no longer exists.",
132
- "no-revision": "This revision no longer exists. Reload the project.",
133
- conflict: "The saved project changed. Reload it and try Resume.",
134
- "idempotency-conflict": "This request ID belongs to another action. Reload the project.",
135
- "invalid-edit": "This section needs attention in Edit project before rerunning.",
136
- "stale-preview": "The required work changed. Try Resume to check it again.",
137
- "invalid-selection": "This section has no generated work to rerun. Use Edit project to change its source.",
138
- "review-required": "Changed supplied content or manual captions need Edit project or optional Advanced rebuild review.",
139
- "cost-ack-required": "Use optional Advanced rebuild review for this request.",
140
- readiness: "Check the provider, model, voice or source files, then try Resume.",
141
- running: "Wait for this section to finish, or Pause the project before rerunning it.",
142
- "accepted-job": "This section cannot be rerun yet.",
143
- "control-changed": "A newer control action took precedence. Check the project before using Resume.",
131
+ "no-project": "This project no longer exists. Go back to Projects to pick another.",
132
+ "no-revision": "The project's saved settings changed since the page loaded. Reload the page, then try again.",
133
+ conflict: "This project changed since the page loaded. Reload the page to see the latest, then use Resume again.",
134
+ "idempotency-conflict": "This action was already sent with different details. Reload the page, then try again.",
135
+ "invalid-edit": "This section has settings that need fixing first. Open the Edit tab, choose Edit project and correct this section, then try again.",
136
+ "stale-preview": "What needs to be made changed since the page loaded. Reload the page, then use Resume again.",
137
+ "invalid-selection": "This section has nothing generated to re-run because its content was supplied by you. To change it, open the Edit tab and choose Edit project.",
138
+ "review-required": "Content you supplied or captions you edited by hand would be replaced. Change them in the Edit tab with Edit project, or use Rebuild affected outputs to confirm the replacement.",
139
+ "cost-ack-required": "This re-run needs you to review its cost first. In the Edit tab, use Rebuild affected outputs.",
140
+ readiness: "Slopify cannot run this yet. Check the provider, model, voice and any files you supplied, then use Resume.",
141
+ running: "This section is still running. Wait for it to finish, or Pause the project, before re-running it.",
142
+ "accepted-job": "This section has a finished provider job that Slopify has not collected yet.",
143
+ "control-changed": "Another action on this project (such as Pause or Cancel) happened at the same time. Reload the page to see its current state, then try again.",
144
144
  };
145
145
  const detail = [
146
146
  messages[result.reason],
147
147
  ...(result.fields ?? []).map((row) => row.message),
148
148
  ...(result.intentRevisionId === undefined
149
149
  ? []
150
- : ["The rerun revision was saved. Use Resume to continue it; do not rerun again."]),
150
+ : [
151
+ "Your re-run request was saved. Use Resume to continue it instead of re-running again.",
152
+ ]),
151
153
  ].join(" ");
152
154
  return problem(c, {
153
155
  status,
@@ -218,7 +220,7 @@ function revisionRequired(c) {
218
220
  return problem(c, {
219
221
  status: 409,
220
222
  title: titleOf(409),
221
- detail: "Open Edit project to save changes, then review the affected rebuild.",
223
+ detail: "This change is now made from the Edit tab. Choose Edit project, save your change, then use Rebuild affected outputs.",
222
224
  extensions: { reason: "revision-required" },
223
225
  });
224
226
  }