cozyclay 3.0.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/INSTALL_FOR_AGENTS.md +146 -0
  3. package/README.md +74 -192
  4. package/THIRD_PARTY_NOTICES.md +0 -21
  5. package/bin/agent/agent-routes.mjs +22 -56
  6. package/bin/agent/agent-runner.mjs +5 -3
  7. package/bin/agent/main-agent.mjs +70 -0
  8. package/bin/agent/main-prompt.mjs +17 -0
  9. package/bin/agent/session-store.mjs +2 -2
  10. package/bin/agent/studio-prompt.mjs +1 -1
  11. package/bin/agent/studio-task-routes.mjs +185 -0
  12. package/bin/agent/studio-tasks.mjs +156 -0
  13. package/bin/agent/studio-tools.mjs +3 -2
  14. package/bin/agent/studio-turn-transaction.mjs +76 -0
  15. package/bin/cozyclay.mjs +5 -7
  16. package/dist/app/index.html +7 -6
  17. package/dist/assets/TaskBoard-DdDXMgZm.js +1 -0
  18. package/dist/assets/app-B2BpfCl2.js +5088 -0
  19. package/dist/assets/app-BlwOpest.css +1 -0
  20. package/dist/assets/compile-worker-D0pKioa0.js +5 -0
  21. package/dist/assets/landing-y3EqDgSZ.js +1 -0
  22. package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  23. package/dist/assets/motion3-compile-B887n77J.js +1 -0
  24. package/dist/assets/scene-asset-sync-COGOlF5T.css +1 -0
  25. package/dist/assets/scene-asset-sync-CQzEdsqu.js +13 -0
  26. package/dist/assets/task-board-BKaM7d-9.js +14 -0
  27. package/dist/assets/workflow-B8Dhzexj.js +179 -0
  28. package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
  29. package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
  30. package/dist/cozyclay-package.json +1 -1
  31. package/dist/greybox-to-video/index.html +2 -3
  32. package/dist/index.html +7 -57
  33. package/dist/llms.txt +2 -1
  34. package/dist/previs-software/index.html +2 -2
  35. package/dist/privacy/index.html +3 -7
  36. package/dist/seedance-camera-control/index.html +2 -2
  37. package/dist/sitemap.xml +5 -5
  38. package/dist/sw.js +5 -1
  39. package/dist/thumbs/blank-stage-dark.webp +0 -0
  40. package/dist/thumbs/blank-stage-light.webp +0 -0
  41. package/dist/workflow/index.html +5 -4
  42. package/mcp/mesh-file.mjs +3 -1
  43. package/mcp/server.mjs +3 -0
  44. package/package.json +3 -3
  45. package/src/App.jsx +50 -281
  46. package/src/analytics.js +3 -148
  47. package/src/app-stage.jsx +1 -1
  48. package/src/asset-pane.jsx +5 -10
  49. package/src/auto-flat.js +10 -0
  50. package/src/bus-tasks.js +11 -5
  51. package/src/command-bus.js +1 -1
  52. package/src/commands/motion3-compile.js +51 -27
  53. package/src/commands/motion3.js +3 -3
  54. package/src/commands/project.js +1 -1
  55. package/src/commands/shot.js +16 -4
  56. package/src/controls.jsx +0 -11
  57. package/src/cuts.js +11 -0
  58. package/src/domains/cast.js +3 -2
  59. package/src/domains/motion.js +3 -10
  60. package/src/domains/playback.js +32 -0
  61. package/src/domains/scenes.js +3 -22
  62. package/src/domains/shots.js +5 -5
  63. package/src/dualview.jsx +11 -15
  64. package/src/hierarchy-panel.css +18 -4
  65. package/src/hierarchy-panel.jsx +12 -11
  66. package/src/kits/held.js +3 -2
  67. package/src/light-blockers.js +2 -2
  68. package/src/main.jsx +5 -0
  69. package/src/motion/ground.js +5 -14
  70. package/src/motion/playback-frame-store.js +79 -0
  71. package/src/motion/playback-probe.js +16 -0
  72. package/src/motion/playback-subject.js +7 -0
  73. package/src/motion/timeline.css +43 -5
  74. package/src/motion/timeline.jsx +4 -3
  75. package/src/motion3/body.js +37 -5
  76. package/src/motion3/carried.js +11 -3
  77. package/src/motion3/compile-job.js +1 -1
  78. package/src/motion3/compiler/compile.js +7 -1
  79. package/src/motion3/compiler/finger-close.js +137 -0
  80. package/src/motion3/compiler/limbs.js +18 -3
  81. package/src/motion3/compiler/pose.js +6 -0
  82. package/src/motion3/compiler/validate-objects.js +17 -2
  83. package/src/motion3/compiler/validate-parts.js +2 -1
  84. package/src/motion3/compiler/vocab.js +4 -4
  85. package/src/motion3/describe.js +2 -2
  86. package/src/motion3/fix/index.js +5 -3
  87. package/src/motion3/flight.js +38 -0
  88. package/src/motion3/free-motion.js +10 -7
  89. package/src/motion3/load.js +125 -0
  90. package/src/motion3/mass.js +23 -0
  91. package/src/motion3/object-keys.js +11 -6
  92. package/src/motion3/object-pose.js +11 -3
  93. package/src/motion3/pipeline.js +3 -0
  94. package/src/motion3/qa/explain.js +3 -1
  95. package/src/motion3/qa/grasp-checks.js +123 -0
  96. package/src/motion3/qa/hints-grasp.js +71 -0
  97. package/src/motion3/qa/index.js +3 -1
  98. package/src/motion3/qa/object-checks.js +7 -2
  99. package/src/motion3/qa/patch.js +2 -1
  100. package/src/motion3/qa/spec-checks.js +10 -1
  101. package/src/motion3/schema/fields.js +3 -3
  102. package/src/motion3/schema/topics.js +9 -8
  103. package/src/motion3/schema/types.js +4 -1
  104. package/src/motion3/seat.js +84 -0
  105. package/src/motion3/settle.js +11 -6
  106. package/src/motion3/shapes.js +70 -0
  107. package/src/motion3/skills/catch.js +71 -0
  108. package/src/motion3/skills/index.js +10 -3
  109. package/src/motion3/skills/info.js +13 -0
  110. package/src/motion3/skills/throw.js +72 -0
  111. package/src/object-catalog.jsx +0 -4
  112. package/src/object-gizmo.jsx +2 -1
  113. package/src/object-path.js +1 -1
  114. package/src/panels/ProjectPanel.jsx +42 -21
  115. package/src/panels/details.css +15 -4
  116. package/src/popover.js +37 -0
  117. package/src/posestudio.jsx +15 -10
  118. package/src/project-browser-art.jsx +19 -0
  119. package/src/project-browser.css +74 -121
  120. package/src/project-browser.jsx +36 -60
  121. package/src/props.jsx +4 -7
  122. package/src/retired-kinds.js +37 -0
  123. package/src/scene-objects.js +3 -7
  124. package/src/scenes.js +4 -5
  125. package/src/settings-menu.jsx +1 -20
  126. package/src/shell/BottomDock.jsx +9 -9
  127. package/src/shell/MenuBar.jsx +1 -9
  128. package/src/shell/PreferencesDialog.jsx +3 -4
  129. package/src/shell/ViewportToolbar.jsx +3 -3
  130. package/src/shell/add-camera-shot.js +11 -0
  131. package/src/shell/glass-regions.css +3 -2
  132. package/src/shell/glass.css +0 -5
  133. package/src/shell/lens-scrub-math.js +51 -0
  134. package/src/shell/lens-scrub.css +73 -0
  135. package/src/shell/lens-scrub.jsx +98 -0
  136. package/src/shell/mode.css +6 -5
  137. package/src/shell/preferences.css +1 -32
  138. package/src/shell/shell.css +18 -1
  139. package/src/shell/studio-shell-context.js +5 -0
  140. package/src/shot-frame.js +26 -0
  141. package/src/stage-canvas-rigs.jsx +7 -8
  142. package/src/stage-characters.jsx +26 -0
  143. package/src/stage-gizmo.js +43 -0
  144. package/src/stage-labels-ko.js +0 -6
  145. package/src/stage-startup-defaults.js +3 -9
  146. package/src/starter-scenes.js +5 -11
  147. package/src/studio-agent-commands.js +2 -2
  148. package/src/studio-agent-protocol.js +2 -7
  149. package/src/studio-app-binding.js +1 -1
  150. package/src/studio-elements.js +1 -1
  151. package/src/styles/themes.css +13 -0
  152. package/src/styles.css +6 -674
  153. package/src/text/text-object.jsx +1 -1
  154. package/src/ui.jsx +8 -16
  155. package/src/use-embed-bridge.js +3 -6
  156. package/src/use-render-activity.js +3 -1
  157. package/src/view-ghosts.jsx +5 -3
  158. package/src/viewport-visible-rect.js +37 -0
  159. package/src/workflow/AgentPanel.jsx +75 -42
  160. package/src/workflow/AgentProgressCard.jsx +1 -1
  161. package/src/workflow/TaskBoard.jsx +102 -0
  162. package/src/workflow/TaskInline.jsx +114 -0
  163. package/src/workflow/agent-client.js +11 -25
  164. package/src/workflow/agent-panel.css +14 -1
  165. package/src/workflow/attachment-image.js +10 -3
  166. package/src/workflow/attachment-video.js +91 -0
  167. package/src/workflow/task-board.css +98 -0
  168. package/src/workflow/task-board.js +169 -0
  169. package/tools/check-eslint.mjs +2 -1
  170. package/tools/check-knip.mjs +2 -1
  171. package/tools/dev/pages/greybox-to-video.html +2 -3
  172. package/tools/dev/pages/previs-software.html +2 -2
  173. package/tools/dev/pages/privacy.html +3 -7
  174. package/tools/dev/pages/seedance-camera-control.html +2 -2
  175. package/tools/dev-full.mjs +11 -2
  176. package/tools/eslint-baseline.json +6 -6
  177. package/tools/knip-baseline.json +0 -11
  178. package/tools/loc-baseline.json +30 -31
  179. package/tools/metrics-snapshot.mjs +43 -5
  180. package/tools/render-start-thumbs.mjs +175 -0
  181. package/tools/run-tests.mjs +7 -14
  182. package/tools/sitemap.mjs +1 -1
  183. package/dist/assets/analytics-BibyE5ar.js +0 -6
  184. package/dist/assets/app-DhRJ-PBz.js +0 -5088
  185. package/dist/assets/app-f-fkV5uP.css +0 -1
  186. package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
  187. package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
  188. package/dist/assets/landing-DYg87MDY.js +0 -1
  189. package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
  190. package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
  191. package/dist/assets/workflow-BSA22Ofr.js +0 -179
  192. package/dist/scenes/city-block.cclayproject +0 -1
  193. package/src/burn-in.js +0 -91
  194. package/src/camera-tutorial.jsx +0 -520
  195. package/src/facing-marks.js +0 -88
  196. package/src/first-shot-handoff.js +0 -44
  197. package/src/panels/motion.css +0 -197
  198. package/src/set-pieces.jsx +0 -183
  199. package/src/tutorial-analytics.js +0 -86
  200. package/src/tutorial-walk.js +0 -17
  201. package/src/use-camera-tutorial.js +0 -50
  202. package/src/use-semantic-state.js +0 -12
  203. package/tools/metrics/archive-traffic.mjs +0 -138
  204. package/tools/perf/ik-camera-drag.mjs +0 -138
@@ -0,0 +1,156 @@
1
+ // Parallel Studio tasks, P2 (the server; plan: ~/.claude/plans/parallel-studio-agents.md, cases by number). The main
2
+ // agent answers the user at once and hands work to tasks; each task is an ordinary Studio agent run (studio-prompt.mjs,
3
+ // its own history) inside task.begin..finish on its scope (src/bus-tasks.js). This file is the board: it queues, starts
4
+ // and ends tasks and tells the main agent what happened, never what it hopes happened.
5
+ // - at most CAP running tasks; the rest wait in order (11)
6
+ // - a task whose scope another running task holds waits for it; the bus is the authority (SCOPE_BUSY at begin) (1)
7
+ // - dependsOn: a task starts after those are done and hears their summaries; a failed one fails it (8)
8
+ // - a second task starts only once the first one's model has answered, or STAGGER ms passed: the shared prompt prefix
9
+ // is then cached once, not written by every task at the same moment (12)
10
+ // - usage and cost per task (13); summaries are capped so the main agent's context stays small (16)
11
+ // - a run whose Studio went away pauses, and resumes when the main agent next sees the same scene; another scene fails
12
+ // it (14)
13
+ // - the same instructions on the same scope as a queued or running task are not started twice (19)
14
+ // - a task that runs past MAX_MS or MAX_TOOLS tool calls is stopped and reported as such (20)
15
+ import { randomUUID } from "node:crypto";
16
+ import { isScopeKey, scopeConflict } from "../../src/bus-task-scopes.js";
17
+
18
+ export const TASK_LIMITS = Object.freeze({ cap: 3, staggerMs: 1500, maxMs: 20 * 60_000, maxTools: 120, summaryChars: 1200, receipts: 8 });
19
+ const ACTIVE = new Set(["queued", "waiting", "starting", "running", "paused"]);
20
+ const ENDED = new Set(["done", "failed", "cancelled"]);
21
+ const norm = text => String(text ?? "").toLowerCase().replace(/\s+/g, " ").trim();
22
+ const clip = (text, n) => (text.length > n ? `${text.slice(0, n - 1)}…` : text);
23
+
24
+ /**
25
+ * run(task, { resume }) runs one task to its end and answers { status: "done" | "failed" | "cancelled" | "lost" |
26
+ * "scopeBusy", reply, receipts, usage, error }; it calls task.onFrame(frame) for every frame. emit(event) tells the
27
+ * main agent (task.queued, task.started, task.done, task.failed, task.cancelled, task.paused, task.resumed).
28
+ */
29
+ export function createTaskBoard({ run, emit = () => {}, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout, limits = {} } = {}) {
30
+ const L = { ...TASK_LIMITS, ...limits }, tasks = new Map();
31
+ let lastStart = null, scheduled = null;
32
+ const list = () => [...tasks.values()];
33
+ const running = () => list().filter(task => ["starting", "running"].includes(task.status));
34
+ const scopes = () => new Map(running().map(task => [task.taskId, { taskId: task.taskId, title: task.title, keys: new Set(task.scope) }]));
35
+ const view = task => ({ taskId: task.taskId, title: task.title, status: task.status, scope: task.scope, ...(task.reason ? { reason: task.reason } : {}), ...(task.turnId ? { turnId: task.turnId } : {}),
36
+ ...(task.dependsOn.length ? { dependsOn: task.dependsOn } : {}), tools: task.tools, usage: task.usage, ...(task.summary ? { summary: task.summary } : {}) });
37
+ const tell = (type, task, extra = {}) => emit({ type, at: now(), ...view(task), ...extra });
38
+
39
+ function dispatch({ title, instructions, scope, reads = [], dependsOn = [] }) {
40
+ if (typeof instructions !== "string" || !instructions.trim()) throw Object.assign(new Error("A task needs instructions."), { code: "INVALID_ARGUMENT" });
41
+ if (!Array.isArray(scope) || !scope.length || scope.some(key => !isScopeKey(key))) throw Object.assign(new Error("scope: one or more keys object:<id>, character:<id>, shot:<id>, stage, timeline or scene."), { code: "INVALID_ARGUMENT" });
42
+ const unknown = dependsOn.find(id => !tasks.has(id));
43
+ if (unknown) throw Object.assign(new Error(`dependsOn: no task ${unknown}.`), { code: "INVALID_ARGUMENT" });
44
+ const same = list().find(task => ACTIVE.has(task.status) && norm(task.instructions) === norm(instructions) && [...task.scope].sort().join() === [...scope].sort().join());
45
+ if (same) return { duplicateOf: same.taskId, ...view(same) };
46
+ const task = { taskId: randomUUID(), title: clip(String(title || instructions).trim(), 120), instructions, scope: [...new Set(scope)], reads, dependsOn,
47
+ status: "queued", tools: 0, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, requests: 0 }, createdAt: now(), warm: false };
48
+ tasks.set(task.taskId, task);
49
+ tell("task.queued", task);
50
+ schedule();
51
+ return view(task);
52
+ }
53
+
54
+ // What still holds a queued task back, or null when it may start now.
55
+ function blocker(task) {
56
+ const failed = task.dependsOn.map(id => tasks.get(id)).find(dep => ["failed", "cancelled"].includes(dep.status));
57
+ if (failed) return { fail: `it depends on task ${failed.taskId} (${failed.title}), which ${failed.status === "failed" ? "failed" : "was cancelled"}` };
58
+ const open = task.dependsOn.map(id => tasks.get(id)).find(dep => dep.status !== "done");
59
+ if (open) return { wait: `waits for task ${open.taskId} (${open.title}) to finish` };
60
+ const held = scopeConflict(task.taskId, task.scope, scopes());
61
+ if (held) return { wait: `${held}` };
62
+ if (running().length >= L.cap) return { wait: `${L.cap} tasks are running; it starts when one ends` };
63
+ return null;
64
+ }
65
+
66
+ function schedule() {
67
+ if (scheduled) return;
68
+ for (const task of list().filter(task => ["queued", "waiting"].includes(task.status)).sort((a, b) => a.createdAt - b.createdAt)) {
69
+ const held = blocker(task);
70
+ if (held?.fail) { end(task, "failed", { reason: held.fail }); continue; }
71
+ if (held) { if (task.reason !== held.wait) { task.status = "waiting"; task.reason = held.wait; tell("task.waiting", task); } continue; }
72
+ // Cache warm-up: the previous start's model has not answered yet, and STAGGER has not passed.
73
+ const wait = lastStart && !lastStart.warm ? lastStart.at + L.staggerMs - now() : 0;
74
+ if (wait > 0) { scheduled = setTimer(() => { scheduled = null; schedule(); }, wait); return; }
75
+ start(task);
76
+ }
77
+ }
78
+
79
+ function rescheduleNow() { if (scheduled) { clearTimer(scheduled); scheduled = null; } schedule(); }
80
+
81
+ function start(task, resume = null) {
82
+ task.status = "starting"; task.reason = undefined; task.startedAt ??= now();
83
+ lastStart = { at: now(), warm: false, taskId: task.taskId };
84
+ const mark = lastStart;
85
+ task.controller = new AbortController();
86
+ const deps = task.dependsOn.map(id => tasks.get(id)).map(dep => ({ taskId: dep.taskId, title: dep.title, summary: dep.summary }));
87
+ task.timer = setTimer(() => stop(task, `it ran past ${Math.round(L.maxMs / 60_000)} minutes`), L.maxMs);
88
+ task.onFrame = frame => {
89
+ if (task.status === "starting") { task.status = "running"; tell(resume ? "task.resumed" : "task.started", task); }
90
+ if (!mark.warm && ["text.delta", "tool.start", "thinking.delta"].includes(frame.type)) { mark.warm = true; rescheduleNow(); }
91
+ if (frame.type === "tool.start" && ++task.tools > L.maxTools) stop(task, `it made more than ${L.maxTools} tool calls`);
92
+ };
93
+ Promise.resolve().then(() => run(task, { resume, deps, signal: task.controller.signal })).then(result => settle(task, result), error => settle(task, { status: "failed", error }));
94
+ }
95
+
96
+ function settle(task, result) {
97
+ clearTimer(task.timer);
98
+ if (lastStart?.taskId === task.taskId) lastStart.warm = true;
99
+ for (const key of Object.keys(task.usage)) if (Number.isFinite(result?.usage?.[key])) task.usage[key] += result.usage[key];
100
+ if (result.status === "scopeBusy") { task.status = "waiting"; task.reason = result.error?.message ?? "its scope is held by another task"; tell("task.waiting", task); }
101
+ else if (result.status === "lost") { task.status = "paused"; task.reason = "Studio disconnected; it resumes when Studio reconnects to the same scene"; task.sceneId = result.sceneId; tell("task.paused", task); }
102
+ else {
103
+ const summary = { reply: clip(String(result.reply ?? "").trim(), L.summaryChars), receipts: (result.receipts ?? []).slice(-L.receipts) };
104
+ // A cancel that arrived after the run finished cannot take it back: it stays done (and can be undone).
105
+ end(task, task.stopReason ? "failed" : task.cancelled && result.status !== "done" ? "cancelled" : result.status, { reason: task.stopReason ?? (result.status === "failed" ? clip(String(result.error?.message ?? result.error ?? "the run failed"), 300) : undefined), summary });
106
+ }
107
+ rescheduleNow();
108
+ }
109
+
110
+ function end(task, status, { reason, summary } = {}) {
111
+ task.status = status; task.reason = reason; task.endedAt = now();
112
+ if (summary) task.summary = summary;
113
+ tell(`task.${status === "done" ? "done" : status}`, task);
114
+ }
115
+
116
+ function stop(task, reason) {
117
+ if (!["starting", "running"].includes(task.status)) return;
118
+ task.stopReason = `stopped: ${reason}; ask the user whether to continue`;
119
+ task.controller.abort(new Error(task.stopReason));
120
+ }
121
+
122
+ return {
123
+ dispatch,
124
+ board: () => list().map(view),
125
+ get: taskId => tasks.get(taskId),
126
+ /** Cancels a queued task, or stops a running one (its keys go back: task.cancel). */
127
+ cancel(taskId) {
128
+ const task = tasks.get(taskId);
129
+ if (!task || ENDED.has(task.status)) return null;
130
+ if (["queued", "waiting", "paused"].includes(task.status)) { end(task, "cancelled", { reason: "cancelled before it ran" }); schedule(); return view(task); }
131
+ task.cancelled = true; task.controller.abort(new Error("cancelled"));
132
+ return view(task);
133
+ },
134
+ /** A Studio context the main agent saw: paused tasks of that scene resume there, others fail. */
135
+ reconnect(sceneId) {
136
+ for (const task of list().filter(task => task.status === "paused")) {
137
+ if (task.sceneId && task.sceneId !== sceneId) end(task, "failed", { reason: "Studio came back on another scene" });
138
+ else start(task, { note: "Studio reconnected after a disconnect. Read the scene again before you continue the task from where you were." });
139
+ }
140
+ schedule();
141
+ },
142
+ running: () => running().map(view),
143
+ dispose() { if (scheduled) clearTimer(scheduled); for (const task of running()) task.controller.abort(new Error("closed")); },
144
+ };
145
+ }
146
+
147
+ /** The header a task's run reads before its instructions (after the cached system prompt and motion reference). */
148
+ export function taskHeader(task, { deps = [], resume = null } = {}) {
149
+ return [`<task id="${task.taskId}">`,
150
+ `You are one of several tasks working on this scene at once. Your scope (the only things you may change): ${task.scope.join(", ")}.`,
151
+ "Everything else is read-only for you: another task or the user may be changing it. A command outside your scope is refused with SCOPE_DENIED; ask for more with run_action task.requestScope { taskId, scope }, and if that answers SCOPE_BUSY, do what you can inside your scope and say what is left.",
152
+ "When you finish, end with a short report for the coordinator in English: what you changed (receipt ids, ids), the QA problems left, and anything not done. The coordinator talks to the user.",
153
+ ...deps.map(dep => `Earlier task ${dep.taskId} (${dep.title}) is done: ${dep.summary?.reply ?? ""}`),
154
+ ...(resume?.note ? [resume.note] : []),
155
+ "</task>", "", task.instructions].join("\n");
156
+ }
@@ -25,7 +25,7 @@ export function createStudioTools({ liveHub, workspaceHandle, session, resolveIm
25
25
  const command = validateStudioCommand({ name, args });
26
26
  const action = name === "run_action" ? declared.get(command.args.action) : undefined;
27
27
  const payload = mutationNames.has(name) && session?.admission
28
- ? { name, args: command.args, commandId: session.admission.commandId(), host: session.admission.host, expectedRevision: session.admission.revision, ...(session.admission.turnId ? { turnId: session.admission.turnId } : {}) }
28
+ ? { name, args: command.args, commandId: session.admission.commandId(), host: session.admission.host, expectedRevision: session.admission.revision, ...(session.admission.turnId ? { turnId: session.admission.turnId } : {}), ...(session.admission.taskId ? { taskId: session.admission.taskId } : {}) }
29
29
  : command.args;
30
30
  Object.assign(scratch, { refusedRevision: payload.expectedRevision });
31
31
  let result;
@@ -38,7 +38,8 @@ export function createStudioTools({ liveHub, workspaceHandle, session, resolveIm
38
38
  } catch (error) {
39
39
  // A STALE_SCENE re-admits whichever family met it, so the retry the
40
40
  // model is told to make is admitted at the live revision.
41
- if (session?.admission && (error?.code === "STALE_SCENE" || (mutationNames.has(name) && error?.code === "UNCERTAIN_APPLY"))) scratch.fresh = await session.admission.refresh();
41
+ // A refresh that fails (the editor went away) keeps the error that happened, not the refresh's.
42
+ if (session?.admission && (error?.code === "STALE_SCENE" || (mutationNames.has(name) && error?.code === "UNCERTAIN_APPLY"))) scratch.fresh = await session.admission.refresh().catch(() => null);
42
43
  throw error;
43
44
  }
44
45
  if (result?.ok === false) {
@@ -0,0 +1,76 @@
1
+ // The editor transaction a Studio run holds while its model works.
2
+ //
3
+ // A single turn (the default): one user message is one editor transaction. Every Studio turn whose editor reports a
4
+ // previsMode opens agent.turn.begin and closes agent.turn.finish, so all of its mutations land as ONE history entry and
5
+ // one receipt frame. A context without a previsMode runs unwrapped (an editor that predates turn transactions).
6
+ //
7
+ // A task (parallel Studio tasks, P2): the run holds task.begin { taskId, scope, reads, title } instead, so other tasks
8
+ // edit other keys of the same scene meanwhile. Its commands carry the taskId and commit one by one (src/bus-tasks.js).
9
+ // A stopped task cancels (its keys go back to where they were); a task that ends, even on an error, finishes and keeps
10
+ // what it did. A task whose Studio went away (the browser reloaded) is neither: the editor that held it is gone.
11
+ //
12
+ // mark() records where a script starts; rollback() undoes only the edits after it and keeps the run open, so earlier
13
+ // edits still land.
14
+ import { randomUUID } from "node:crypto";
15
+
16
+ // The Studio went away (a reload, a closed tab): no editor answers, or it went while a command ran (UNCERTAIN_APPLY: the
17
+ // command may or may not have landed; mcp/live-hub.mjs).
18
+ export const LOST_STUDIO = new Set(["LIVE_HUB_UNAVAILABLE", "STALE_HANDLE", "NO_EDITOR", "UNCERTAIN_APPLY"]);
19
+
20
+ export function studioTransaction({ hub, context, turnId, admission, task = null, signal, plain = false }) {
21
+ // The main agent holds no transaction (plain): its one quick edit opens a task of its own.
22
+ const wrapped = !plain && Boolean(task || context.scene?.previsMode);
23
+ const host = Object.fromEntries(["workspaceId", "documentEpoch", "sceneId", "sceneEpoch"].map(key => [key, context.host[key]]));
24
+ const verb = name => task ? `task.${name === "begin" ? "begin" : name}` : `agent.turn.${name}`;
25
+ const control = async (name, extra = {}) => {
26
+ const args = task ? { taskId: task.taskId, ...extra } : { turnId };
27
+ const receipt = await hub.command("run_action", {
28
+ name: "run_action", args: { action: verb(name), args }, ...(task ? { taskId: task.taskId } : { turnId }),
29
+ commandId: randomUUID(), host, expectedRevision: admission.revision,
30
+ }, context.host.workspaceHandle);
31
+ if (!receipt.ok) throw Object.assign(new Error(receipt.message), { code: receipt.code });
32
+ return receipt;
33
+ };
34
+ const owner = task ? "taskId" : "turnId", id = task ? task.taskId : turnId;
35
+ const open = async () => {
36
+ await control("begin", task ? { scope: task.scope, ...(task.reads?.length ? { reads: task.reads } : {}), ...(task.title ? { title: task.title.slice(0, 120) } : {}) } : {});
37
+ admission[owner] = id;
38
+ };
39
+ return {
40
+ mark: async () => (admission[owner] === id ? control("mark") : null),
41
+ async rollback() {
42
+ if (admission[owner] !== id) return null;
43
+ const receipt = await control("rollback");
44
+ await admission.refresh();
45
+ return receipt;
46
+ },
47
+ run: start => async function* (input) {
48
+ if (!wrapped) { yield* start(input); return; }
49
+ await open();
50
+ let terminal, lost = false, finishError = null;
51
+ try {
52
+ for await (const frame of start(input)) {
53
+ if (frame.type === "tool.done" && !frame.ok && LOST_STUDIO.has(String(frame.error ?? "").split(":")[0])) lost = true;
54
+ if (frame.type === "done") terminal = frame; else yield frame;
55
+ }
56
+ } finally {
57
+ // Finish only the transaction this run opened (a rollback keeps it open).
58
+ if (admission[owner] === id) {
59
+ delete admission[owner];
60
+ const stopped = task && (signal?.aborted || input?.signal?.aborted);
61
+ if (!(task && (lost || task.lost))) {
62
+ try {
63
+ const receipt = await control(stopped ? "cancel" : "finish");
64
+ if (receipt.authored || task) yield { type: "receipt", receipt };
65
+ } catch (error) {
66
+ // The editor that held the task is gone: there is nothing left to finish.
67
+ if (!task || !LOST_STUDIO.has(error.code)) finishError = error;
68
+ }
69
+ }
70
+ }
71
+ }
72
+ if (finishError) throw finishError;
73
+ if (terminal) yield terminal;
74
+ },
75
+ };
76
+ }
package/bin/cozyclay.mjs CHANGED
@@ -147,9 +147,7 @@ const HELP = `cozyclay - browser-based 3D staging studio
147
147
  cclay update install the latest cozyclay globally (npm install -g)
148
148
  npx cozyclay --port 5200 serve on another port
149
149
  npx cozyclay --no-open do not open a browser
150
- npx cozyclay --scene city-block
151
- open the studio on a bundled starter scene
152
- (the set from the cozyclay.org tutorial)
150
+ npx cozyclay --scene <id> open the studio on a bundled starter scene
153
151
  npx cozyclay --no-star never ask about starring the repo
154
152
  npx cozyclay --no-update-check
155
153
  do not look for a newer release
@@ -483,11 +481,11 @@ server.listen({ port: opts.port, host: "127.0.0.1", ipv6Only: false }, () => {
483
481
  // The package exists to open the studio, which the site serves from /app/.
484
482
  // Landing on "/" would greet someone who just typed `npx cozyclay` with a
485
483
  // marketing page.
486
- // --scene <id> continues from a bundled starter scene (the landing-page
487
- // tutorial hands people this exact command). Only a bare id is accepted
488
- // here; the studio resolves it to dist/scenes/<id>.cclayproject.
484
+ // --scene <id> opens a bundled starter scene. Only a bare id is accepted
485
+ // here; the studio resolves it to dist/scenes/<id>.cclayproject and ignores
486
+ // an id it does not ship, with a console warning.
489
487
  const sceneParam = opts.scene && /^[a-z0-9-]+$/i.test(opts.scene) ? `?scene=${encodeURIComponent(opts.scene)}` : "";
490
- if (opts.scene && !sceneParam) console.error(`cozyclay: --scene expects a starter id such as city-block (got ${opts.scene}); opening the studio without it.`);
488
+ if (opts.scene && !sceneParam) console.error(`cozyclay: --scene expects a bare starter id (got ${opts.scene}); opening the studio without it.`);
491
489
  const url = `http://127.0.0.1:${opts.port}/app/${sceneParam}`;
492
490
  console.log(`CozyClay is running at ${url}`);
493
491
  console.log("Use a Chromium-based browser — Safari and Firefox are not supported.");
@@ -34,14 +34,15 @@
34
34
  <title>CozyClay Studio — 3D staging and previs in your browser</title>
35
35
 
36
36
  <script defer src="https://static.cloudflareinsights.com/beacon.min.js" data-cf-beacon='{"token": "6a4384d1375d4ffc8a30a6346a8ad1a0"}'></script>
37
- <script type="module" crossorigin src="/assets/app-DhRJ-PBz.js"></script>
37
+ <script type="module" crossorigin src="/assets/app-B2BpfCl2.js"></script>
38
38
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
39
- <link rel="modulepreload" crossorigin href="/assets/analytics-BibyE5ar.js">
40
- <link rel="modulepreload" crossorigin href="/assets/first-shot-handoff-Qx7SKl38.js">
39
+ <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-P2Xu9kJm.js">
40
+ <link rel="modulepreload" crossorigin href="/assets/task-board-BKaM7d-9.js">
41
41
  <link rel="modulepreload" crossorigin href="/assets/fflate.module-KbGj5-H6.js">
42
- <link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-DBYQBR0T.js">
43
- <link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-_S8NQl5B.css">
44
- <link rel="stylesheet" crossorigin href="/assets/app-f-fkV5uP.css">
42
+ <link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-CQzEdsqu.js">
43
+ <link rel="modulepreload" crossorigin href="/assets/motion3-compile-B887n77J.js">
44
+ <link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-COGOlF5T.css">
45
+ <link rel="stylesheet" crossorigin href="/assets/app-BlwOpest.css">
45
46
  </head>
46
47
  <body>
47
48
  <div id="root">
@@ -0,0 +1 @@
1
+ import{r as e}from"./rolldown-runtime-hePW80VL.js";import{Pr as t,c as n,d as r,f as i,l as a,m as o,o as s,v as c,xi as l}from"./task-board-BKaM7d-9.js";import{o as u,r as d,t as f}from"./motion3-compile-B887n77J.js";var p=e(l(),1),m=2e3,h=(e,t)=>new Promise(n=>{let r=setTimeout(n,e);t.addEventListener(`abort`,()=>{clearTimeout(r),n()},{once:!0})});function g(e,n){(0,p.useEffect)(()=>{let r=new Map;for(let[t,i]of Object.entries(e))for(let e of i)r.set(e,n[t]??t);let i=()=>{for(let e of document.querySelectorAll(`[data-node-id]`)){let n=r.get(e.getAttribute(`data-node-id`));n?(e.setAttribute(`data-task-scope`,n),e.setAttribute(`title`,t(`In task: ${n}`,`작업 중: ${n}`))):e.hasAttribute(`data-task-scope`)&&(e.removeAttribute(`data-task-scope`),e.removeAttribute(`title`))}};i();let a=new MutationObserver(i);return a.observe(document.body,{childList:!0,subtree:!0}),()=>{a.disconnect(),r.clear(),i()}},[e,n])}function _({sessionId:e,pulse:t,labelOf:l,onMainNote:_}){let v=(0,p.useMemo)(()=>s(),[]),y=u(),[b,x]=(0,p.useReducer)((e,t)=>t.seed?o(e,t.seed):t.reset?n():r(e,t.event),void 0,n),[S,C]=(0,p.useState)({}),[w,T]=(0,p.useState)(f),E=(0,p.useRef)(0),D=(0,p.useRef)(_);(0,p.useEffect)(()=>{E.current=b.after,D.current=_},[b.after,_]),(0,p.useEffect)(()=>{x({reset:!0}),E.current=0},[e]),(0,p.useEffect)(()=>d(T),[]),(0,p.useEffect)(()=>y.subscribe(e=>{typeof e?.type!=`string`||!e.type.startsWith(`task.`)||(C(t=>i(t,e)),(e.type===`task.userEdit`||e.type===`task.undone`)&&x({event:e}))}),[y]),(0,p.useEffect)(()=>{if(!e)return;let t=new AbortController;return(async()=>{for(;!t.signal.aborted;){try{let{tasks:n}=await v.board(e);x({seed:n}),await v.events(e,E.current,e=>{if(e.type===`main.turn`&&e.turnId){let n=``;v.frames(e.turnId,e=>{e.type===`text.delta`&&(n+=e.text??``),e.type===`tool.start`&&(n=``)},t.signal).then(()=>D.current?.(n),()=>{})}x({event:e})},t.signal)}catch{}await h(m,t.signal)}})(),()=>t.abort()},[v,e,t]);let O=b.order.filter(e=>b.tasks[e]?.status===`running`&&b.tasks[e]?.turnId),k=O.map(e=>`${e}:${b.tasks[e].turnId}`).join();return(0,p.useEffect)(()=>{let e=O.map(e=>{let t=new AbortController,{turnId:n}=b.tasks[e];return v.frames(n,t=>{let n=a(t,l);n&&x({event:{type:`task.progress`,taskId:e,status:`running`,progress:n}})},t.signal).catch(()=>{}),t});return()=>e.forEach(e=>e.abort())},[v,k,l]),g(S,(0,p.useMemo)(()=>Object.fromEntries(b.order.map(e=>[e,b.tasks[e].title])),[b])),(0,p.useEffect)(()=>{c.set({client:v,labelOf:l,undo:async e=>{let t=await y.run(`task.undo`,{taskId:e});if(t?.ok===!1)throw Error(t.message??t.code);x({event:{type:`task.undone`,taskId:e}})}})},[y,v,l]),(0,p.useEffect)(()=>{c.set({board:b,activity:w})},[b,w]),(0,p.useEffect)(()=>()=>c.set({board:n(),open:null}),[e]),null}export{_ as default};