cozyclay 3.2.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/INSTALL_FOR_AGENTS.md +4 -4
- package/README.md +4 -2
- package/bin/agent/agent-failure-codes.mjs +34 -8
- package/bin/agent/agent-routes.mjs +6 -6
- package/bin/agent/agent-runner.mjs +16 -13
- package/bin/agent/agent-stream.mjs +26 -3
- package/bin/agent/main-prompt.mjs +1 -1
- package/bin/agent/studio-prompt.mjs +2 -2
- package/bin/agent/supervisor-followup.mjs +34 -0
- package/bin/agent/supervisor.mjs +6 -6
- package/bin/cozyclay.mjs +23 -60
- package/bin/star-prompt.mjs +15 -14
- package/bin/studio-routes.mjs +6 -3
- package/bin/telemetry-state.mjs +5 -7
- package/bin/update-check.mjs +61 -7
- package/bin/update-install.mjs +16 -3
- package/bin/update-restart.mjs +95 -0
- package/bin/update-routes.mjs +25 -3
- package/dist/app/index.html +13 -7
- package/dist/assets/TaskBoard-tgO0MNKL.js +1 -0
- package/dist/assets/analytics-CaTUuLPM.js +1 -0
- package/dist/assets/app-CnDkB5WO.css +1 -0
- package/dist/assets/app-DJt06Oyb.js +5144 -0
- package/dist/assets/{compile-worker-DRWRa9pr.js → compile-worker-D1pl6G7x.js} +5 -5
- package/dist/assets/landing-2Fy7DU41.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-BxaM0Xwl.js +1 -0
- package/dist/assets/scene-asset-sync-CLGNbEBq.js +13 -0
- package/dist/assets/task-board-UVGTZLRF.js +15 -0
- package/dist/assets/workflow-CSiBlX1s.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +4 -1
- package/dist/privacy/index.html +9 -6
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +4 -4
- package/mcp/tool-handlers.mjs +14 -16
- package/package.json +2 -1
- package/src/App.jsx +37 -70
- package/src/analytics-controls.js +25 -3
- package/src/analytics-strict.js +16 -4
- package/src/analytics.js +32 -26
- package/src/app-version.js +13 -0
- package/src/asset-drop.js +39 -0
- package/src/asset-pane.css +34 -0
- package/src/asset-pane.jsx +4 -35
- package/src/bus-tasks.js +1 -1
- package/src/character-kind.js +8 -6
- package/src/click-friction.js +5 -4
- package/src/command-bus-telemetry.js +8 -7
- package/src/command-bus.js +2 -16
- package/src/commands/cast.js +2 -1
- package/src/commands/motion3.js +3 -1
- package/src/commands/objects.js +2 -37
- package/src/commands/project.js +3 -3
- package/src/commands/sculpt-compare.js +53 -0
- package/src/content-tile.jsx +36 -0
- package/src/domains/cast.js +7 -6
- package/src/domains/motion.js +6 -5
- package/src/domains/scenes.js +41 -31
- package/src/execution-telemetry.js +7 -3
- package/src/export-menu-telemetry.js +26 -0
- package/src/finger-handles.jsx +128 -0
- package/src/friction-hint.js +82 -0
- package/src/live-control.js +2 -1
- package/src/locale-zh/panels.js +20 -0
- package/src/locale-zh/shell.js +4 -0
- package/src/locale-zh/workspace.js +6 -0
- package/src/motion/find-bone.js +13 -0
- package/src/motion/fingers.js +170 -0
- package/src/motion/fingertip-pick.js +21 -0
- package/src/motion/ik-exposure.js +48 -0
- package/src/motion/ik.js +4 -11
- package/src/motion3/compiler/head-lead.js +4 -2
- package/src/motion3/hybrid-hint.js +19 -0
- package/src/motion3/load.js +19 -12
- package/src/motion3/qa/hints-contact.js +12 -0
- package/src/motion3/qa/patch.js +4 -1
- package/src/motion3/qa/verdict.js +7 -2
- package/src/motion3/repair.js +28 -8
- package/src/motion3/skills/catch.js +2 -1
- package/src/panels/FingerPanel.jsx +38 -0
- package/src/panels/RigControlPanel.jsx +9 -1
- package/src/panels/SculptPanel.jsx +12 -12
- package/src/panels/SculptPartTree.jsx +46 -0
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/panels/sculpt.css +11 -0
- package/src/pose-fade.js +7 -3
- package/src/posestudio.jsx +14 -27
- package/src/project-browser.css +6 -0
- package/src/project-browser.jsx +9 -18
- package/src/project-start-choices.jsx +45 -0
- package/src/project.js +16 -12
- package/src/proxy-figure-shape.js +11 -0
- package/src/proxy-figure.jsx +25 -18
- package/src/scene-assets.js +17 -13
- package/src/scenes.js +22 -11
- package/src/sculpt-blueprint.js +56 -0
- package/src/sculpt-mode.js +5 -0
- package/src/sculpt-part-edit.js +115 -0
- package/src/sculpt-part-gizmo.jsx +159 -0
- package/src/sculpt-part-outline.jsx +72 -0
- package/src/sculpt-pick.js +53 -0
- package/src/sculpt-run-text.js +51 -0
- package/src/sculpt-run.js +10 -46
- package/src/sculpt-shelf-file.js +92 -0
- package/src/sculpt-shelf-tiles.jsx +69 -0
- package/src/sculpt-shelf.js +108 -0
- package/src/sculpt-stage.jsx +39 -2
- package/src/sculpt-thumb.js +72 -0
- package/src/sculpt-views.js +75 -0
- package/src/semantic-edit.js +0 -87
- package/src/settings-menu.jsx +2 -2
- package/src/shell/DetailsSlot.jsx +5 -6
- package/src/shell/LibrarySlot.jsx +8 -10
- package/src/shell/MenuBar.jsx +6 -6
- package/src/shell/PreferencesDialog.jsx +2 -3
- package/src/shell/ViewportToolbar.jsx +15 -18
- package/src/shell/add-character-entries.js +13 -0
- package/src/shell/pose-controls.js +32 -0
- package/src/shell/preference-pages.jsx +2 -11
- package/src/shell/storyboard-caption.js +21 -0
- package/src/shell/studio-shell-context.js +1 -0
- package/src/shortcut-friction.js +2 -1
- package/src/site/landing-analytics.js +28 -0
- package/src/studio-actions.js +3 -2
- package/src/studio-agent-protocol.js +4 -4
- package/src/studio-app-binding.js +4 -3
- package/src/update/UpdateChip.jsx +21 -2
- package/src/update/UpdateDialog.jsx +26 -6
- package/src/update/update-client.js +123 -17
- package/src/update/update-telemetry.js +15 -3
- package/src/use-sculpt-mode.js +62 -8
- package/src/workflow/agent-failure-codes.js +28 -9
- package/src/workflow/agent-turn-telemetry.js +1 -1
- package/src/workflow/task-board.js +2 -2
- package/tools/dev/page-shell.py +1 -1
- package/tools/dev/pages/greybox-to-video.html +21 -2
- package/tools/dev/pages/previs-software.html +3 -3
- package/tools/dev/pages/privacy.html +9 -6
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/loc-baseline.json +10 -10
- package/tools/run-tests.mjs +45 -69
- package/tools/select-tests.mjs +119 -0
- package/tools/test-durations.json +455 -0
- package/tools/test-graph.mjs +181 -0
- package/tools/test-shards.mjs +78 -0
- package/tools/tutorial/README.md +74 -0
- package/tools/tutorial/assemble.mjs +43 -0
- package/tools/tutorial/captions.mjs +43 -0
- package/tools/tutorial/cdp.mjs +55 -0
- package/tools/tutorial/cursor.mjs +159 -0
- package/tools/tutorial/episode.mjs +173 -0
- package/tools/tutorial/episodes/beginner-guide/00-install.mjs +42 -0
- package/tools/tutorial/episodes/beginner-guide/01-start.mjs +50 -0
- package/tools/tutorial/episodes/beginner-guide/02-layout.mjs +67 -0
- package/tools/tutorial/episodes/beginner-guide/03-stage.mjs +58 -0
- package/tools/tutorial/episodes/beginner-guide/04-motion.mjs +98 -0
- package/tools/tutorial/episodes/beginner-guide/05-camera.mjs +47 -0
- package/tools/tutorial/episodes/beginner-guide/06-export.mjs +59 -0
- package/tools/tutorial/episodes/beginner-guide/steps.mjs +32 -0
- package/tools/tutorial/episodes/beginner-guide.mjs +17 -0
- package/tools/tutorial/episodes/cast.mjs +12 -0
- package/tools/tutorial/episodes/first-video.mjs +121 -0
- package/tools/tutorial/episodes/stub-tour.mjs +97 -0
- package/tools/tutorial/launch.mjs +63 -0
- package/tools/tutorial/mux.mjs +61 -0
- package/tools/tutorial/overlay.mjs +142 -0
- package/tools/tutorial/record.mjs +156 -0
- package/tools/tutorial/scenes/terminal.html +63 -0
- package/tools/tutorial/steps.mjs +111 -0
- package/tools/tutorial/terminal.mjs +43 -0
- package/tools/tutorial/voice.mjs +49 -0
- package/tools/update-test-durations.mjs +42 -0
- package/tools/verify-package-tarball.mjs +18 -12
- package/dist/assets/TaskBoard-BUjjuvm5.js +0 -1
- package/dist/assets/app-Dige7q3X.css +0 -1
- package/dist/assets/app-DxoI941o.js +0 -5144
- package/dist/assets/motion3-compile-B4SX4TGx.js +0 -1
- package/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/assets/scene-asset-sync-LeI3eSnO.js +0 -13
- package/dist/assets/task-board-5PKn7ZaA.js +0 -15
- package/dist/assets/workflow-Dg9jCKOq.js +0 -179
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
// (bin/update-routes.mjs) read and driven from the page. Pure apart from the
|
|
3
3
|
// injected fetch, timers, storage and window, so test/verify-update-client.mjs
|
|
4
4
|
// drives it under Node. A page without window.__COZYCLAY_RUNTIME__ (the hosted
|
|
5
|
-
// site, Vite dev) makes no request at all.
|
|
5
|
+
// site, Vite dev) makes no request at all. Tabs share a dismiss, an install
|
|
6
|
+
// start and a phase change at once (U2): over a BroadcastChannel, or through a
|
|
7
|
+
// storage event where there is none. A hint only makes a tab fetch now; the
|
|
8
|
+
// launcher's answer stays the only phase a tab shows. Restart now (U2b, #960)
|
|
9
|
+
// hands the launcher to the new version and waits for it to answer.
|
|
6
10
|
export const UPDATE_URL = "/__cozyclay/update";
|
|
11
|
+
export const RESTART_URL = "/__cozyclay/update/restart";
|
|
12
|
+
export const RESTART_WAIT_MS = 30_000;
|
|
13
|
+
export const RESTART_POLL_MS = 1000;
|
|
7
14
|
export const DISMISS_KEY = "cozyclay:update-dismissed";
|
|
8
15
|
export const POLL_MS = 6 * 60 * 60 * 1000;
|
|
9
16
|
export const VISIBLE_STALE_MS = 60 * 60 * 1000;
|
|
10
17
|
export const UPDATING_POLL_MS = 2000;
|
|
18
|
+
export const CHANNEL_NAME = "cozyclay:update";
|
|
19
|
+
export const HINT_KEY = "cozyclay:update-hint";
|
|
11
20
|
|
|
12
21
|
const VERSION = /^\d+\.\d+\.\d+$/;
|
|
13
22
|
const KINDS = new Set(["global", "npx", "clone"]);
|
|
@@ -42,16 +51,17 @@ export function normalizeUpdateInfo(body) {
|
|
|
42
51
|
phase,
|
|
43
52
|
errorClass: phase === "failed" ? (ERROR_CLASSES.has(body.errorClass) ? body.errorClass : "other") : null,
|
|
44
53
|
durationMs: Number.isFinite(body.durationMs) && body.durationMs >= 0 ? body.durationMs : null,
|
|
54
|
+
installed: version(body.installed),
|
|
45
55
|
// Opened in a new tab, so only the project's own release page is accepted.
|
|
46
56
|
notesUrl: latest && body.notesUrl === `${NOTES_PREFIX}${latest}` ? body.notesUrl : null,
|
|
47
57
|
command: kind === "npx" ? NPX_COMMAND : GLOBAL_COMMAND,
|
|
48
58
|
};
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
async function request(fetchImpl, init, win) {
|
|
61
|
+
async function request(fetchImpl, init, win, url = UPDATE_URL) {
|
|
52
62
|
if (!hasLauncherRuntime(win) || typeof fetchImpl !== "function") return null;
|
|
53
63
|
try {
|
|
54
|
-
const response = await fetchImpl(
|
|
64
|
+
const response = await fetchImpl(url, { cache: "no-store", credentials: "same-origin", ...init });
|
|
55
65
|
if (!response?.ok) return null;
|
|
56
66
|
return normalizeUpdateInfo(await response.json());
|
|
57
67
|
} catch {
|
|
@@ -65,6 +75,16 @@ export const fetchUpdateInfo = (fetchImpl = globalThis.fetch?.bind(globalThis),
|
|
|
65
75
|
/** POST: ask the launcher to install `latest`. The 202 body, or null (403/409/network). No body is sent. */
|
|
66
76
|
export const startUpdate = (fetchImpl = globalThis.fetch?.bind(globalThis), win = globalThis.window) => request(fetchImpl, { method: "POST" }, win);
|
|
67
77
|
|
|
78
|
+
/** POST: hand the launcher to the installed version. The 202 body, or null (403/409/network). No body is sent. */
|
|
79
|
+
export const requestRestart = (fetchImpl = globalThis.fetch?.bind(globalThis), win = globalThis.window) => request(fetchImpl, { method: "POST" }, win, RESTART_URL);
|
|
80
|
+
|
|
81
|
+
/** After the new launcher answers: reload only a saved project. True when it reloaded. */
|
|
82
|
+
export function reloadAfterRestart({ dirty, reload }) {
|
|
83
|
+
if (dirty) return false;
|
|
84
|
+
reload();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
/** patch | minor | major: how far `current` is behind `latest`. */
|
|
69
89
|
export function behindOf(current, latest) {
|
|
70
90
|
const [a, b] = [current, latest].map((value) => (version(value) ?? "0.0.0").split(".").map(Number));
|
|
@@ -83,16 +103,24 @@ export function updateBlocked({ agentRunning = false, exportStatus = null, recSt
|
|
|
83
103
|
|
|
84
104
|
const HIDDEN = Object.freeze({ view: "hidden", actions: [] });
|
|
85
105
|
|
|
86
|
-
|
|
106
|
+
const RESTART_ACTIONS = { restarting: [], restarted: ["reload"], restart_timeout: ["copy_command"] };
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The chip and dialog view for one poll result and the local state: { dismissed }, and { restart }
|
|
110
|
+
* (restarting | restarted | restart_timeout) once Restart now ran here or in another tab.
|
|
111
|
+
*/
|
|
87
112
|
export function nextUpdateState(info, local = {}) {
|
|
88
113
|
if (!info || !info.enabled) return HIDDEN;
|
|
89
114
|
const base = {
|
|
90
115
|
current: info.current, latest: info.latest, kind: info.kind, notesUrl: info.notesUrl,
|
|
91
116
|
command: info.command, canSelfUpdate: info.canSelfUpdate, errorClass: null,
|
|
92
117
|
};
|
|
118
|
+
if (RESTART_ACTIONS[local.restart]) return { ...base, latest: local.target ?? info.latest, view: local.restart, command: RESTART_COMMAND, actions: RESTART_ACTIONS[local.restart] };
|
|
93
119
|
const oneClick = info.kind === "global" && info.canSelfUpdate;
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
// "Installing X" and "Updated to X" name the version npm installs, which the registry may have passed since.
|
|
121
|
+
const installed = info.installed ?? info.latest;
|
|
122
|
+
if (info.phase === "updating") return { ...base, latest: installed, view: "updating", actions: [] };
|
|
123
|
+
if (info.phase === "restart_needed") return { ...base, latest: installed, view: "restart_needed", command: RESTART_COMMAND, actions: ["restart", "copy_command"] };
|
|
96
124
|
if (info.phase === "failed") {
|
|
97
125
|
return { ...base, view: "failed", errorClass: info.errorClass ?? "other", command: GLOBAL_COMMAND,
|
|
98
126
|
actions: oneClick ? ["retry", "copy_command"] : ["copy_command"] };
|
|
@@ -115,6 +143,13 @@ export function writeDismissed(latest, storage = safeStorage()) {
|
|
|
115
143
|
try { storage.setItem(DISMISS_KEY, latest); return true; } catch { return false; }
|
|
116
144
|
}
|
|
117
145
|
|
|
146
|
+
const HINTS = new Set(["dismiss", "install", "phase", "restart"]);
|
|
147
|
+
|
|
148
|
+
// A missing or throwing BroadcastChannel is null: hints then travel as a storage event.
|
|
149
|
+
function openUpdateChannel() {
|
|
150
|
+
try { return typeof globalThis.BroadcastChannel === "function" ? new globalThis.BroadcastChannel(CHANNEL_NAME) : null; } catch { return null; }
|
|
151
|
+
}
|
|
152
|
+
|
|
118
153
|
const defaultTimers = {
|
|
119
154
|
set: (fn, ms) => setTimeout(fn, ms),
|
|
120
155
|
clear: (timer) => clearTimeout(timer),
|
|
@@ -131,40 +166,97 @@ const defaultTimers = {
|
|
|
131
166
|
* The poll loop: once after the first frame and idle, every 6 h, on a visible
|
|
132
167
|
* tab whose last check is over 1 h old, every 2 s while installing. `onState`
|
|
133
168
|
* gets { state, info } after every change. Returns { dismiss, update, stop }.
|
|
169
|
+
* `channel` is a BroadcastChannel-shaped object (default: one is opened when
|
|
170
|
+
* the browser has it); null sends the other tabs' hints through `storage`.
|
|
171
|
+
* restart() and a restart hint poll every 1 s, failures included, until the
|
|
172
|
+
* launcher's `current` is the installed version, for up to 30 s.
|
|
134
173
|
*/
|
|
135
174
|
export function createUpdatePoller({
|
|
136
175
|
fetchImpl = globalThis.fetch?.bind(globalThis), onState = () => {}, timers = defaultTimers,
|
|
137
|
-
storage = safeStorage(), win = globalThis.window, doc = globalThis.document,
|
|
176
|
+
storage = safeStorage(), win = globalThis.window, doc = globalThis.document, channel: injected,
|
|
138
177
|
} = {}) {
|
|
139
|
-
const noop = { dismiss() {}, update: async () => null, stop() {} };
|
|
178
|
+
const noop = { dismiss() {}, update: async () => null, restart: async () => null, stop() {} };
|
|
140
179
|
if (!hasLauncherRuntime(win)) return noop;
|
|
141
|
-
|
|
142
|
-
let
|
|
143
|
-
|
|
180
|
+
const channel = injected === undefined ? openUpdateChannel() : injected;
|
|
181
|
+
let info = null, timer = null, stopped = false, inflight = null, lastCheck = -Infinity, hints = 0;
|
|
182
|
+
let dismissed = readDismissed(storage), restart = null, target = null, following = null;
|
|
183
|
+
const emit = () => { if (!stopped) onState({ state: nextUpdateState(info, { dismissed, restart, target }), info }); };
|
|
144
184
|
const schedule = () => {
|
|
145
185
|
if (stopped) return;
|
|
146
186
|
if (timer !== null) timers.clear(timer);
|
|
147
187
|
timer = timers.set(check, info?.phase === "updating" ? UPDATING_POLL_MS : POLL_MS);
|
|
148
188
|
};
|
|
149
|
-
|
|
150
|
-
|
|
189
|
+
// Tell the other tabs; a lost hint only costs them the wait for their next poll.
|
|
190
|
+
const hint = (type, latest = null) => {
|
|
191
|
+
try {
|
|
192
|
+
if (channel) channel.postMessage({ type, latest });
|
|
193
|
+
else if (type !== "dismiss") storage?.setItem(HINT_KEY, `${type}:${timers.now()}:${(hints += 1)}:${version(latest) ?? ""}`);
|
|
194
|
+
} catch { /* best effort */ }
|
|
195
|
+
};
|
|
196
|
+
// Emitted first, so this tab's update:finished claim is in storage before any other tab re-polls.
|
|
197
|
+
// A change a hint led to is not hinted again: the other tabs had the same hint.
|
|
198
|
+
const apply = (next, cause = "phase") => {
|
|
199
|
+
if (!next) return;
|
|
200
|
+
const changed = info !== null && info.phase !== next.phase;
|
|
201
|
+
info = next;
|
|
202
|
+
emit();
|
|
203
|
+
if (cause === "install" || (changed && cause === "phase")) hint(cause);
|
|
204
|
+
};
|
|
205
|
+
function check(cause = "phase") {
|
|
151
206
|
if (stopped) return Promise.resolve(info);
|
|
152
207
|
if (inflight) return inflight;
|
|
153
208
|
// An early check (a visible tab) replaces the pending poll; the schedule never doubles.
|
|
154
209
|
if (timer !== null) { timers.clear(timer); timer = null; }
|
|
155
210
|
lastCheck = timers.now();
|
|
156
211
|
inflight = fetchUpdateInfo(fetchImpl, win);
|
|
157
|
-
return inflight.then((next) => { inflight = null; apply(next); schedule(); return info; });
|
|
212
|
+
return inflight.then((next) => { inflight = null; apply(next, cause); schedule(); return info; });
|
|
158
213
|
}
|
|
159
214
|
const onVisible = () => {
|
|
160
215
|
if (doc?.visibilityState === "visible" && timers.now() - lastCheck > VISIBLE_STALE_MS) void check();
|
|
161
216
|
};
|
|
217
|
+
// A fetch already in flight may predate the hint, so one more follows it.
|
|
218
|
+
const refresh = () => (inflight ? inflight.then(() => check("hinted")) : check("hinted"));
|
|
219
|
+
// The old launcher stops answering for a moment, so a failed fetch is only another try.
|
|
220
|
+
const followRestart = (to) => {
|
|
221
|
+
if (following || !to) return following ?? Promise.resolve(null);
|
|
222
|
+
restart = "restarting";
|
|
223
|
+
target = to;
|
|
224
|
+
emit();
|
|
225
|
+
const deadline = timers.now() + RESTART_WAIT_MS;
|
|
226
|
+
following = (async () => {
|
|
227
|
+
for (;;) {
|
|
228
|
+
await new Promise((done) => timers.set(done, RESTART_POLL_MS));
|
|
229
|
+
if (stopped) return null;
|
|
230
|
+
const next = await fetchUpdateInfo(fetchImpl, win);
|
|
231
|
+
if (next?.current === to) { info = next; restart = "restarted"; break; }
|
|
232
|
+
if (timers.now() >= deadline) { restart = "restart_timeout"; break; }
|
|
233
|
+
}
|
|
234
|
+
emit();
|
|
235
|
+
return restart === "restarted" ? "ok" : "timeout";
|
|
236
|
+
})();
|
|
237
|
+
return following;
|
|
238
|
+
};
|
|
239
|
+
// The version the launcher installed, not the registry's latest: a newer release may appear while it waits.
|
|
240
|
+
const restartTarget = (latest) => version(latest) ?? (info?.phase === "restart_needed" ? info.installed ?? info.latest : null);
|
|
241
|
+
const onHint = (type, latest = null) => {
|
|
242
|
+
if (stopped || !HINTS.has(type)) return;
|
|
243
|
+
if (type === "restart") return void followRestart(restartTarget(latest));
|
|
244
|
+
if (type !== "dismiss") return void refresh();
|
|
245
|
+
dismissed = readDismissed(storage) ?? version(latest) ?? dismissed;
|
|
246
|
+
emit();
|
|
247
|
+
};
|
|
248
|
+
const onMessage = (event) => onHint(event?.data?.type, event?.data?.latest);
|
|
162
249
|
const onStorage = (event) => {
|
|
250
|
+
if (event?.key === HINT_KEY) { const [type, , , latest] = String(event.newValue ?? "").split(":"); return onHint(type, latest || null); }
|
|
163
251
|
if (event?.key !== DISMISS_KEY && event?.key !== null) return;
|
|
164
252
|
dismissed = readDismissed(storage);
|
|
165
253
|
emit();
|
|
166
254
|
};
|
|
167
|
-
try {
|
|
255
|
+
try {
|
|
256
|
+
doc?.addEventListener?.("visibilitychange", onVisible);
|
|
257
|
+
win?.addEventListener?.("storage", onStorage);
|
|
258
|
+
channel?.addEventListener?.("message", onMessage);
|
|
259
|
+
} catch { /* no listeners: the 6 h poll still runs */ }
|
|
168
260
|
timers.afterFirstFrame(() => { if (!stopped) void check(); });
|
|
169
261
|
return {
|
|
170
262
|
dismiss(latest) {
|
|
@@ -172,17 +264,31 @@ export function createUpdatePoller({
|
|
|
172
264
|
writeDismissed(latest, storage);
|
|
173
265
|
dismissed = latest;
|
|
174
266
|
emit();
|
|
267
|
+
hint("dismiss", latest);
|
|
175
268
|
},
|
|
176
269
|
async update() {
|
|
177
270
|
const next = await startUpdate(fetchImpl, win);
|
|
178
|
-
apply(next);
|
|
271
|
+
apply(next, "install");
|
|
179
272
|
schedule();
|
|
180
273
|
return next;
|
|
181
274
|
},
|
|
275
|
+
// Resolves to the outcome (ok | timeout), or null when the launcher refused.
|
|
276
|
+
async restart() {
|
|
277
|
+
const to = restartTarget(null);
|
|
278
|
+
if (!to || following) return following;
|
|
279
|
+
if (!(await requestRestart(fetchImpl, win))) return null;
|
|
280
|
+
hint("restart", to);
|
|
281
|
+
return followRestart(to);
|
|
282
|
+
},
|
|
182
283
|
stop() {
|
|
183
284
|
stopped = true;
|
|
184
285
|
if (timer !== null) timers.clear(timer);
|
|
185
|
-
try {
|
|
286
|
+
try {
|
|
287
|
+
doc?.removeEventListener?.("visibilitychange", onVisible);
|
|
288
|
+
win?.removeEventListener?.("storage", onStorage);
|
|
289
|
+
channel?.removeEventListener?.("message", onMessage);
|
|
290
|
+
channel?.close?.();
|
|
291
|
+
} catch { /* best effort */ }
|
|
186
292
|
},
|
|
187
293
|
};
|
|
188
294
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// update:notice_shown, update:clicked
|
|
1
|
+
// update:notice_shown, update:clicked, update:finished (the U1 update notice) and
|
|
2
|
+
// update:restarted (Restart now, #960).
|
|
2
3
|
// Only closed values leave: the install kind, how far behind (patch/minor/major),
|
|
3
4
|
// the button, the outcome, the npm error class and a duration bucket. Never a
|
|
4
5
|
// version string, a path, a command or npm's output. All three are passive
|
|
@@ -10,7 +11,8 @@ import { bucketLong } from "../analytics-strict.js";
|
|
|
10
11
|
const CLICKS_PER_SESSION = 10;
|
|
11
12
|
const FINISHED_PER_SESSION = 3;
|
|
12
13
|
const NOTICE_KINDS = new Set(["global", "npx"]);
|
|
13
|
-
const CLICK_ACTIONS = new Set(["update_now", "copy_command", "later", "dismiss", "release_notes"]);
|
|
14
|
+
const CLICK_ACTIONS = new Set(["update_now", "copy_command", "later", "dismiss", "release_notes", "restart"]);
|
|
15
|
+
const RESTART_OUTCOMES = new Set(["ok", "timeout"]);
|
|
14
16
|
// The install this browser already reported, shared by the tabs of one profile:
|
|
15
17
|
// several tabs (and several polls) see the same terminal phase, the event leaves once.
|
|
16
18
|
const REPORTED_KEY = "cozyclay:update-reported";
|
|
@@ -31,7 +33,7 @@ export function noteNoticeShown(state) {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
/** A notice button: update_now (also Try again), copy_command, later, dismiss or
|
|
36
|
+
/** A notice button: update_now (also Try again), copy_command, later, dismiss, release_notes or restart. */
|
|
35
37
|
export function noteUpdateClicked(action, kind) {
|
|
36
38
|
try {
|
|
37
39
|
if (!CLICK_ACTIONS.has(action) || !NOTICE_KINDS.has(kind)) return;
|
|
@@ -41,6 +43,16 @@ export function noteUpdateClicked(action, kind) {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** Restart now ended in the tab that pressed it: the new launcher answered (ok) or 30 s passed (timeout). Once per session. */
|
|
47
|
+
export function noteUpdateRestarted(outcome) {
|
|
48
|
+
try {
|
|
49
|
+
if (!RESTART_OUTCOMES.has(outcome)) return;
|
|
50
|
+
if (admit("update:restarted", 1)) send("update:restarted", { outcome });
|
|
51
|
+
} catch {
|
|
52
|
+
// Telemetry must never affect the restart.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
function claim(key) {
|
|
45
57
|
if (reportedHere.has(key)) return false;
|
|
46
58
|
reportedHere.add(key);
|
package/src/use-sculpt-mode.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { createAgentTransport } from "./workflow/agent-client.js";
|
|
3
3
|
import { requestAgentImage, stylizeErrorMessage } from "./agent-image-client.js";
|
|
4
|
-
import { blueprintErrorMessage, blueprintRequest } from "./sculpt-blueprint.js";
|
|
5
|
-
import {
|
|
4
|
+
import { blueprintErrorMessage, blueprintRequest, blueprintViewImages } from "./sculpt-blueprint.js";
|
|
5
|
+
import { browserSculptImages } from "./commands/sculpt-images.js";
|
|
6
|
+
import { agentSessionReady, chatgptSignedIn, createCommandTap, sculptModelChoice, sculptTargetFromSelection, storeSculptModel } from "./sculpt-mode.js";
|
|
6
7
|
import { SCULPT_KIND } from "./scene-objects.js";
|
|
8
|
+
import { overlayOwnsEscape, sculptPartTree } from "./sculpt-pick.js";
|
|
9
|
+
import { GIZMO_HOTKEYS } from "./stage-startup-defaults.js";
|
|
10
|
+
import { ko } from "./locale.js";
|
|
7
11
|
|
|
8
|
-
export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectId }) {
|
|
12
|
+
export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectId, appContext }) {
|
|
9
13
|
// Sculpt mode (#739): one sculpt on an isolated turntable, the Sculpt panel
|
|
10
14
|
// and its agent run. The target is the sculpt selected on entry, or a new one
|
|
11
15
|
// the first run creates; picking another sculpt while here retargets.
|
|
@@ -17,6 +21,8 @@ export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectI
|
|
|
17
21
|
const [sculptRun, setSculptRun] = useState(null);
|
|
18
22
|
const sculptRunRef = useRef(null);
|
|
19
23
|
const [sculptAgentStatus, setSculptAgentStatus] = useState("unknown");
|
|
24
|
+
// Whether the sidecar holds a ChatGPT token: the blueprint's image model needs one (#970).
|
|
25
|
+
const [sculptChatgpt, setSculptChatgpt] = useState(false);
|
|
20
26
|
// The model Sculpt runs on (its own pick, remembered apart from the chat model) and the list it comes from.
|
|
21
27
|
const [sculptModels, setSculptModels] = useState({ state: "loading", providers: [], models: [] });
|
|
22
28
|
const [sculptModelPick, setSculptModelPick] = useState({ model: "", effort: null });
|
|
@@ -47,8 +53,8 @@ export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectI
|
|
|
47
53
|
let live = true;
|
|
48
54
|
const read = () => {
|
|
49
55
|
sculptTransport.status().then(
|
|
50
|
-
(status) => { if (live) setSculptAgentStatus(agentSessionReady(status) ? "ready" : "signed-out"); },
|
|
51
|
-
() => { if (live) setSculptAgentStatus("signed-out"); },
|
|
56
|
+
(status) => { if (live) { setSculptAgentStatus(agentSessionReady(status) ? "ready" : "signed-out"); setSculptChatgpt(chatgptSignedIn(status)); } },
|
|
57
|
+
() => { if (live) { setSculptAgentStatus("signed-out"); setSculptChatgpt(false); } },
|
|
52
58
|
);
|
|
53
59
|
// The Agent panel's catalogue: a key added or removed elsewhere changes what can run.
|
|
54
60
|
sculptTransport.models().then(
|
|
@@ -65,6 +71,50 @@ export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectI
|
|
|
65
71
|
window.addEventListener("focus", read);
|
|
66
72
|
return () => { live = false; window.removeEventListener("focus", read); };
|
|
67
73
|
}, [sculptMode, sculptTransport]);
|
|
74
|
+
// Part picking (#740 S7): one part of the target, chosen on the turntable or
|
|
75
|
+
// in the panel's tree. A new target or leaving the mode drops it; Esc drops
|
|
76
|
+
// it before Esc reaches the scene selection, unless an open menu or dialog takes that Esc.
|
|
77
|
+
const [sculptPart, setSculptPart] = useState(null);
|
|
78
|
+
useEffect(() => { setSculptPart(null); }, [sculptMode, sculptTargetId]);
|
|
79
|
+
// The part gizmo (#740 S8): its tool, and the last refusal or notice it shows under the tree.
|
|
80
|
+
const [partTool, setPartTool] = useState("move");
|
|
81
|
+
const [partNotice, setPartNotice] = useState(null);
|
|
82
|
+
useEffect(() => { setPartNotice(null); }, [sculptPart?.partId, sculptPart?.mirrored, sculptTargetId]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!sculptMode || !sculptPart) return undefined;
|
|
85
|
+
const onKey = (event) => {
|
|
86
|
+
if (event.target?.closest?.("input, textarea, select, [contenteditable='true']")) return;
|
|
87
|
+
// Delete with a part picked would take the whole sculpt: refused, Esc first.
|
|
88
|
+
if (event.key === "Delete" || event.key === "Backspace") {
|
|
89
|
+
event.stopPropagation();
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
setPartNotice(ko("Delete removes the whole sculpt. Press Esc to deselect the part first.", "Delete는 조형물 전체를 지워요. 먼저 Esc로 부품 선택을 해제하세요.", "Delete 会删除整个雕塑。请先按 Esc 取消部件选择。"));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (event.key !== "Escape" || overlayOwnsEscape()) return;
|
|
95
|
+
event.stopPropagation();
|
|
96
|
+
setSculptPart(null);
|
|
97
|
+
};
|
|
98
|
+
window.addEventListener("keydown", onKey, true);
|
|
99
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
100
|
+
}, [sculptMode, sculptPart]);
|
|
101
|
+
const sculptRecipe = sculptTarget?.recipe;
|
|
102
|
+
const sculptTree = useMemo(() => sculptPartTree(sculptRecipe), [sculptRecipe]);
|
|
103
|
+
// A recipe edit that removed the part leaves nothing selected.
|
|
104
|
+
const selectedPart = sculptPart && sculptTree.some((row) => row.id === sculptPart.partId) ? sculptPart : null;
|
|
105
|
+
// W/E/R: the selected part's move, rotate and scale; with no part, the turntable views.
|
|
106
|
+
const onKey = (code) => (selectedPart ? setPartTool(GIZMO_HOTKEYS[code]) : requestSculptView({ KeyW: "front", KeyE: "side", KeyR: "back" }[code]));
|
|
107
|
+
const tools = [["W", "move", ko("Move part", "부품 이동", "移动部件")], ["E", "rotate", ko("Rotate part", "부품 회전", "旋转部件")], ["R", "scale", ko("Scale part (Shift: uniform)", "부품 크기 (Shift: 균등)", "缩放部件(Shift:等比)")]]
|
|
108
|
+
.map(([key, id, name]) => ({ key, id, name, active: partTool === id, pick: () => setPartTool(id) }));
|
|
109
|
+
// One drag, one write: the command refuses what cannot be drawn and its reason is shown.
|
|
110
|
+
const commit = (edit) => {
|
|
111
|
+
if (!edit || !sculptTarget || !appContext) return false;
|
|
112
|
+
let receipt;
|
|
113
|
+
try { receipt = appContext.bus.run("object.sculpt", { id: sculptTarget.id, edits: [edit] }, { origin: "ui" }); } catch (error) { receipt = { ok: false, message: error?.message }; }
|
|
114
|
+
setPartNotice(receipt?.ok === false ? `${ko("Edit refused", "편집 거부", "编辑被拒绝")}: ${receipt.message ?? ""}` : null);
|
|
115
|
+
return receipt?.ok !== false;
|
|
116
|
+
};
|
|
117
|
+
const sculptParts = { tree: sculptTree, selected: selectedPart, onSelect: setSculptPart, tool: partTool, tools, onKey, commit, notice: partNotice };
|
|
68
118
|
// The blueprint step: the reference turned into a four-view model sheet by one image request.
|
|
69
119
|
const [sculptBlueprint, setSculptBlueprint] = useState({ state: "idle", pose: "keep", notes: "", sheet: null, error: null, startedAt: null });
|
|
70
120
|
const blueprintAbortRef = useRef(null);
|
|
@@ -78,7 +128,11 @@ export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectI
|
|
|
78
128
|
try {
|
|
79
129
|
const result = await requestAgentImage(blueprintRequest({ reference: sculptReference, pose: sculptBlueprint.pose, notes: sculptBlueprint.notes }), { signal: controller.signal });
|
|
80
130
|
if (blueprintAbortRef.current !== controller) return;
|
|
81
|
-
|
|
131
|
+
// Cut into views (#970): side and back go to Start next to the reference; a view whose mask fails is dropped with a notice.
|
|
132
|
+
let cropped;
|
|
133
|
+
try { cropped = await blueprintViewImages(result.dataUrl, browserSculptImages); } catch (error) { cropped = { views: [], notices: [`The sheet could not be cut into views: ${error?.message || error}`] }; }
|
|
134
|
+
if (blueprintAbortRef.current !== controller) return;
|
|
135
|
+
setSculptBlueprint((current) => ({ ...current, state: "ready", sheet: { dataUrl: result.dataUrl, width: result.width, height: result.height, ms: Date.now() - startedAt, views: cropped.views, notices: cropped.notices } }));
|
|
82
136
|
} catch (error) {
|
|
83
137
|
if (blueprintAbortRef.current !== controller) return;
|
|
84
138
|
const cancelled = controller.signal.aborted;
|
|
@@ -91,5 +145,5 @@ export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectI
|
|
|
91
145
|
controller?.abort();
|
|
92
146
|
setSculptBlueprint((current) => ({ ...current, state: current.sheet ? "ready" : "idle" }));
|
|
93
147
|
}
|
|
94
|
-
return { sculptMode, sculptCamRef, sculptView, sculptReference, setSculptReference, sculptRun, setSculptRun, sculptRunRef, sculptAgentStatus, sculptModels, sculptModelPick, chooseSculptModel, sculptTransport, sculptTap, sculptTarget, setSculptTargetId, requestSculptView, sculptBlueprint, setSculptBlueprint, generateSculptBlueprint, cancelSculptBlueprint };
|
|
148
|
+
return { sculptMode, sculptCamRef, sculptView, sculptReference, setSculptReference, sculptRun, setSculptRun, sculptRunRef, sculptAgentStatus, sculptChatgpt, sculptModels, sculptModelPick, chooseSculptModel, sculptTransport, sculptTap, sculptTarget, setSculptTargetId, requestSculptView, sculptBlueprint, setSculptBlueprint, generateSculptBlueprint, cancelSculptBlueprint, sculptParts };
|
|
95
149
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The closed `agent:turn_failed.failure_code` vocabulary, browser side. The
|
|
2
2
|
// sidecar keeps an identical table in bin/agent/agent-failure-codes.mjs (it
|
|
3
3
|
// cannot import src/); test/verify-agent-execution.mjs holds both to the same
|
|
4
|
-
// answers. Only the structured code and HTTP status are read,
|
|
4
|
+
// answers. Only the error's name, structured code and HTTP status are read,
|
|
5
|
+
// never a message.
|
|
5
6
|
//
|
|
6
7
|
// key_refused the model provider rejected the credentials (provider
|
|
7
8
|
// 401/403, structured or message-only: runner `unauthorized`)
|
|
@@ -14,9 +15,15 @@
|
|
|
14
15
|
// LIVE_HUB_UNAVAILABLE: no connected editor)
|
|
15
16
|
// busy a turn is already running, or the target is busy
|
|
16
17
|
// invalid_request the sidecar rejected the request (400, INVALID_*, other 4xx)
|
|
17
|
-
// upstream provider
|
|
18
|
-
//
|
|
19
|
-
|
|
18
|
+
// upstream provider trouble: HTTP 5xx, or the runner's `overloaded`,
|
|
19
|
+
// `upstream`, `truncated` (and the provider's `server_error`)
|
|
20
|
+
// network the request never got an HTTP answer: a fetch TypeError
|
|
21
|
+
// (offline, refused, DNS) or a socket code (ECONNRESET,
|
|
22
|
+
// ENOTFOUND, ...), on the error or its `cause`
|
|
23
|
+
// unknown anything else, including an error whose fields throw
|
|
24
|
+
// aborted / tool_failed are assigned by the turn, not by this table
|
|
25
|
+
// Rows before the network/unknown split carry `upstream` for all three.
|
|
26
|
+
export const AGENT_FAILURE_CODES = new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "network", "unknown",
|
|
20
27
|
"key_refused", "stale_scene", "capability_missing", "busy", "invalid_request"]);
|
|
21
28
|
|
|
22
29
|
const BY_CODE = {
|
|
@@ -24,24 +31,36 @@ const BY_CODE = {
|
|
|
24
31
|
STALE_SCENE: "stale_scene", STALE_TARGET: "stale_scene", STALE_ENVIRONMENT: "stale_scene",
|
|
25
32
|
CAPABILITY_MISSING: "capability_missing", LIVE_HUB_UNAVAILABLE: "capability_missing",
|
|
26
33
|
busy: "busy", TARGET_BUSY: "busy", SCOPE_BUSY: "busy", TARGET_NOT_READY: "busy",
|
|
34
|
+
// A sidecar frame whose code is already this table's fallback answer passes through.
|
|
35
|
+
network: "network", unknown: "unknown",
|
|
27
36
|
};
|
|
37
|
+
// The runner's provider codes read as upstream, after the status: a 4xx beside one is the request's fault.
|
|
38
|
+
const PROVIDER_CODES = new Set(["overloaded", "upstream", "truncated", "server_error"]);
|
|
39
|
+
const NETWORK_CODES = new Set(["ECONNRESET", "ECONNREFUSED", "ECONNABORTED", "ENOTFOUND", "EAI_AGAIN", "ETIMEDOUT", "EPIPE",
|
|
40
|
+
"ENETUNREACH", "ENETDOWN", "EHOSTUNREACH", "UND_ERR_SOCKET", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"]);
|
|
41
|
+
const codeOf = (value) => (typeof value?.code === "string" ? value.code : "");
|
|
28
42
|
|
|
29
|
-
/** `{ code, status }` -> failure code. Foreign getters
|
|
43
|
+
/** `{ name, code, status }` -> failure code. Foreign getters read as unknown. */
|
|
30
44
|
export function transportFailureCode(error) {
|
|
31
45
|
try {
|
|
32
|
-
const code =
|
|
46
|
+
const code = codeOf(error);
|
|
33
47
|
const status = error?.status;
|
|
34
48
|
if (Object.hasOwn(BY_CODE, code)) return BY_CODE[code];
|
|
35
49
|
if (code.startsWith("INVALID_")) return "invalid_request";
|
|
36
50
|
if (status === 401 || status === 403) return "auth";
|
|
37
51
|
if (status === 429) return "rate_limited";
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
if (Number.isInteger(status) && status >= 400 && status < 500) return "invalid_request";
|
|
53
|
+
if (Number.isInteger(status) && status >= 500 && status < 600) return "upstream";
|
|
54
|
+
if (PROVIDER_CODES.has(code)) return "upstream";
|
|
55
|
+
if (Number.isInteger(status)) return "unknown";
|
|
56
|
+
if (NETWORK_CODES.has(code) || NETWORK_CODES.has(codeOf(error?.cause)) || error?.name === "TypeError") return "network";
|
|
57
|
+
return "unknown";
|
|
58
|
+
} catch { return "unknown"; }
|
|
40
59
|
}
|
|
41
60
|
|
|
42
61
|
/** A refused turn: the sidecar's `{ error: { code } }` body plus the HTTP status. */
|
|
43
62
|
export async function refusalFailureCode(response) {
|
|
44
63
|
let code;
|
|
45
64
|
try { const body = await response.clone().json(); code = typeof body?.error?.code === "string" ? body.error.code : undefined; } catch { /* status alone */ }
|
|
46
|
-
try { return transportFailureCode({ code, status: response?.status }); } catch { return "
|
|
65
|
+
try { return transportFailureCode({ code, status: response?.status }); } catch { return "unknown"; }
|
|
47
66
|
}
|
|
@@ -142,7 +142,7 @@ export function startAgentTurn({ surface, capture, now, correlationId = null })
|
|
|
142
142
|
return {
|
|
143
143
|
turnId,
|
|
144
144
|
cancel: () => finish("cancelled", "aborted"),
|
|
145
|
-
fail: (code) => finish("failed", AGENT_FAILURE_CODES.has(code) ? code : "
|
|
145
|
+
fail: (code) => finish("failed", AGENT_FAILURE_CODES.has(code) ? code : "unknown"),
|
|
146
146
|
/** A tool.start / tool.done frame the panel sees (after replay dedupe). */
|
|
147
147
|
tool(event) {
|
|
148
148
|
if (!terminal) advisory(() => tally.note(event), null);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The Studio task board in the agent panel (parallel tasks, P3). Task mode is a per-browser switch (the panel's Tasks
|
|
1
|
+
// The Studio task board in the agent panel (parallel tasks, P3). Task mode is a per-browser switch, on by default (the panel's Tasks
|
|
2
2
|
// toggle) or the server's COZYCLAY_STUDIO_TASKS flag: the chat then talks to the main agent (POST /agent/main), which
|
|
3
3
|
// answers at once and dispatches tasks; this board shows them. Pure parts here (the reducer, labels, the switch's
|
|
4
4
|
// storage); the network side is createTaskBoardClient; the view is TaskBoard.jsx.
|
|
@@ -13,7 +13,7 @@ import { parseSseChunk } from "./agent-client.js";
|
|
|
13
13
|
|
|
14
14
|
const TASKS_KEY = "cozyclay.agent.tasks";
|
|
15
15
|
export function readStoredTaskMode(storage = globalThis.localStorage) {
|
|
16
|
-
try { return storage?.getItem(TASKS_KEY)
|
|
16
|
+
try { return storage?.getItem(TASKS_KEY) !== "off"; } catch { return true; } // on unless the author turned it off
|
|
17
17
|
}
|
|
18
18
|
export function storeTaskMode(enabled, storage = globalThis.localStorage) {
|
|
19
19
|
try { storage?.setItem(TASKS_KEY, enabled ? "on" : "off"); } catch { /* storage may be unavailable */ }
|
package/tools/dev/page-shell.py
CHANGED
|
@@ -12,7 +12,7 @@ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
|
12
12
|
PAGES_DIR = os.path.join(os.path.dirname(__file__), "pages")
|
|
13
13
|
|
|
14
14
|
NAV = """ <nav>
|
|
15
|
-
<a href="
|
|
15
|
+
<a href="/app/">Try it</a>
|
|
16
16
|
<a href="/greybox-to-video/">Greybox to video</a>
|
|
17
17
|
<a href="/previs-software/">Previs software</a>
|
|
18
18
|
<a href="/ai-camera-control/">Camera control</a>
|
|
@@ -72,6 +72,25 @@
|
|
|
72
72
|
</div>
|
|
73
73
|
</section>
|
|
74
74
|
|
|
75
|
+
<section>
|
|
76
|
+
<h2>What the models actually accept</h2>
|
|
77
|
+
<p>
|
|
78
|
+
Seedance 2.5's product page names this input: "Provides advanced capabilities such as white-model control
|
|
79
|
+
and green-screen editing, combined with professional camera movement and performance blocking." Its showcase
|
|
80
|
+
prompt directs the model to "Use the white-model reference video as the sole guide for camera movement,
|
|
81
|
+
pacing, framing, subject motion, and blocking." That white-model reference video is a plain RGB blockout
|
|
82
|
+
clip, which is exactly what the keyframe pack exports. MiniMax H3 reference-to-video, the Wan 3.0 hosted
|
|
83
|
+
<code>reference_video</code>, LTX Desktop's "3D Render to Photoreal" ("keeping the exact composition, camera
|
|
84
|
+
move and layout of the input") and fal render-to-real all consume the same clip.
|
|
85
|
+
</p>
|
|
86
|
+
<p>
|
|
87
|
+
The caveat is Kling Motion Control: "Avoid cuts, camera movement, etc.; otherwise, the video may be
|
|
88
|
+
trimmed." So send it a single character, one continuous take.
|
|
89
|
+
</p>
|
|
90
|
+
<p class="note">None of the top hosted APIs (Seedance, Kling, Veo 3.1, Runway Act-Two, MiniMax)
|
|
91
|
+
documents a depth or pose control-video input.</p>
|
|
92
|
+
</section>
|
|
93
|
+
|
|
75
94
|
<section>
|
|
76
95
|
<h2>The block-then-generate loop</h2>
|
|
77
96
|
<ol>
|
|
@@ -107,11 +126,11 @@
|
|
|
107
126
|
<h2>Do it in the browser</h2>
|
|
108
127
|
<p>
|
|
109
128
|
CozyClay is open source previs that runs in a Chromium tab. It exists for exactly this loop: block,
|
|
110
|
-
camera, export, generate. The <a href="
|
|
129
|
+
camera, export, generate. The <a href="/app/">browser studio</a> runs it in a tab, and
|
|
111
130
|
<code>npx cozyclay</code> runs it on your machine.
|
|
112
131
|
</p>
|
|
113
132
|
<div class="cta">
|
|
114
|
-
<a class="btn primary" href="
|
|
133
|
+
<a class="btn primary" href="/app/">Try it in your browser</a>
|
|
115
134
|
<a class="btn ghost" href="https://github.com/NomaDamas/CozyClay">View on GitHub</a>
|
|
116
135
|
</div>
|
|
117
136
|
</section>
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
<p>
|
|
65
65
|
The honest split is time, not money. Blender can do everything on this page and will take an
|
|
66
66
|
afternoon to learn just the camera. CozyClay does one thing (block, camera, cut, export) and expects
|
|
67
|
-
you to be productive in the first ten minutes; the <a href="
|
|
68
|
-
runs it in
|
|
67
|
+
you to be productive in the first ten minutes; the <a href="/app/">browser studio</a>
|
|
68
|
+
runs it in a tab.
|
|
69
69
|
</p>
|
|
70
70
|
</section>
|
|
71
71
|
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
<li><strong>Everything, given time:</strong> Blender.</li>
|
|
116
116
|
</ul>
|
|
117
117
|
<div class="cta">
|
|
118
|
-
<a class="btn primary" href="
|
|
118
|
+
<a class="btn primary" href="/app/">Try CozyClay in your browser</a>
|
|
119
119
|
<a class="btn ghost" href="https://github.com/NomaDamas/CozyClay">Source on GitHub</a>
|
|
120
120
|
</div>
|
|
121
121
|
</section>
|