cozyclay 3.1.4 → 3.1.6
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 +104 -0
- package/INSTALL_FOR_AGENTS.md +3 -0
- package/README.md +5 -0
- package/bin/agent/agent-failure-codes.mjs +31 -0
- package/bin/agent/agent-routes.mjs +5 -10
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/cozyclay.mjs +16 -64
- package/bin/studio-routes.mjs +69 -0
- package/bin/update-check.mjs +53 -5
- package/bin/update-install.mjs +136 -0
- package/bin/update-routes.mjs +80 -0
- package/dist/ai-camera-control/index.html +1 -1
- package/dist/app/index.html +5 -6
- package/dist/assets/{TaskBoard-U1Yl-4F1.js → TaskBoard-DwXhsJXz.js} +1 -1
- package/dist/assets/app-BLS1Zaru.js +5144 -0
- package/dist/assets/app-CEP7jzXB.css +1 -0
- package/dist/assets/compile-worker-or7djI2v.js +6 -0
- package/dist/assets/{motion3-compile-DDG3wklD.js → motion3-compile-Du7UXrWn.js} +1 -1
- package/dist/assets/scene-asset-sync-t45pWCyq.js +13 -0
- package/dist/assets/task-board-CkLZGPkv.js +15 -0
- package/dist/assets/workflow-DJwOUzqJ.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +3 -3
- package/dist/index.html +533 -264
- package/dist/media/cozyclay-mark.png +0 -0
- package/dist/media/cozyclay-truck-demo-poster.jpg +0 -0
- package/dist/media/step-move-poster.jpg +0 -0
- package/dist/media/step-shoot-poster.jpg +0 -0
- package/dist/media/step-stage-poster.jpg +0 -0
- package/dist/media/truck-greybox-poster.jpg +0 -0
- package/dist/media/truck-seedance-poster.jpg +0 -0
- package/dist/previs-software/index.html +4 -4
- package/dist/privacy/index.html +46 -19
- package/dist/seedance-camera-control/index.html +3 -3
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +3 -4
- package/mcp/project-camera.mjs +43 -0
- package/package.json +2 -1
- package/src/App.jsx +59 -70
- package/src/analytics-controls.js +17 -0
- package/src/analytics-friction.js +124 -0
- package/src/analytics-strict.js +139 -0
- package/src/analytics-values.js +49 -0
- package/src/analytics.js +37 -133
- package/src/asset-pane.jsx +4 -5
- package/src/click-friction.js +101 -0
- package/src/command-bus-telemetry.js +155 -0
- package/src/command-bus.js +10 -8
- package/src/commands/motion3.js +7 -2
- package/src/controls.jsx +3 -2
- package/src/crash-telemetry.js +45 -0
- package/src/domains/cast.js +2 -11
- package/src/domains/motion.js +49 -38
- package/src/domains/objects.js +3 -0
- package/src/domains/scene-undo.js +27 -0
- package/src/domains/scenes.js +1 -1
- package/src/error-boundary.jsx +2 -0
- package/src/export-attempt.js +100 -0
- package/src/export-workspace.js +12 -1
- package/src/first-success-guide.jsx +11 -7
- package/src/guide-telemetry.js +60 -0
- package/src/hierarchy-panel.jsx +2 -1
- package/src/live-control.js +2 -9
- package/src/locale-zh/domains.js +1 -0
- package/src/main.jsx +5 -0
- package/src/motion/timeline.jsx +9 -5
- package/src/motion-draft-telemetry.js +62 -0
- package/src/motion3/compiler/channels.js +7 -5
- package/src/motion3/compiler/compile.js +6 -2
- package/src/motion3/compiler/finger-close.js +84 -7
- package/src/motion3/compiler/head-lead.js +70 -0
- package/src/motion3/compiler/head-steady.js +1 -1
- package/src/motion3/compiler/limbs.js +10 -3
- package/src/motion3/compiler/pose.js +1 -1
- package/src/motion3/compiler/validate.js +5 -3
- package/src/motion3/pipeline.js +4 -0
- package/src/motion3/qa/explain.js +2 -1
- package/src/motion3/qa/finger-checks.js +80 -0
- package/src/motion3/qa/gait-observe.js +88 -0
- package/src/motion3/qa/grasp-checks.js +3 -0
- package/src/motion3/qa/hints-grasp.js +8 -0
- package/src/motion3/qa/index.js +2 -1
- package/src/motion3/qa/motion-life.js +26 -3
- package/src/motion3/schema/topics.js +1 -1
- package/src/playback-telemetry.js +107 -0
- package/src/post-turn-edits.js +62 -0
- package/src/search-friction.js +49 -0
- package/src/semantic-edit.js +19 -6
- package/src/shell/BoardSlot.jsx +3 -3
- package/src/shell/BottomDock.jsx +1 -1
- package/src/shell/DetailsSlot.jsx +1 -1
- package/src/shell/MenuBar.jsx +13 -13
- package/src/shell/StudioShell.jsx +7 -1
- package/src/shell/TopBar.jsx +2 -0
- package/src/shell/ViewportToolbar.jsx +2 -2
- package/src/shortcut-friction.js +122 -0
- package/src/stage-canvas-rigs.jsx +3 -4
- package/src/studio-telemetry.js +97 -0
- package/src/thrash-friction.js +90 -0
- package/src/update/UpdateChip.jsx +86 -0
- package/src/update/UpdateDialog.jsx +147 -0
- package/src/update/update-client.js +188 -0
- package/src/update/update-telemetry.js +78 -0
- package/src/update/update.css +94 -0
- package/src/use-webgl-available.js +35 -0
- package/src/workflow/AgentPanel.jsx +29 -30
- package/src/workflow/CozySceneNode.jsx +1 -1
- package/src/workflow/LoginPanel.jsx +15 -5
- package/src/workflow/agent-client.js +10 -91
- package/src/workflow/agent-failure-codes.js +47 -0
- package/src/workflow/agent-turn-telemetry.js +222 -0
- package/src/workflow/connect-telemetry.js +86 -0
- package/src/workflow/prompt-telemetry.js +105 -0
- package/tools/dev/pages/privacy.html +44 -17
- package/tools/loc-baseline.json +10 -10
- package/tools/run-tests.mjs +8 -8
- package/tools/verify-package-tarball.mjs +29 -1
- package/dist/assets/app-BdRlMf4J.css +0 -1
- package/dist/assets/app-KXdgBC-S.js +0 -5144
- package/dist/assets/compile-worker-oy_P9HHh.js +0 -6
- package/dist/assets/landing-y3EqDgSZ.js +0 -1
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +0 -1
- package/dist/assets/scene-asset-sync-Bz59Ghdz.js +0 -13
- package/dist/assets/task-board-DFfNa0xg.js +0 -15
- package/dist/assets/workflow-BMJGd_gZ.js +0 -179
- package/dist/media/playground-poster.jpg +0 -0
|
@@ -44,6 +44,8 @@ import { TurnSteps } from "./AgentTurnSteps.jsx";
|
|
|
44
44
|
import { PlanCard } from "./AgentPlanCard.jsx";
|
|
45
45
|
import { ReplyText } from "./AgentReplyText.jsx";
|
|
46
46
|
import { stepLabel, turnLayout } from "./agent-turn.js";
|
|
47
|
+
import { connectAuthState, connectFinished, connectOpened, connectStarted, modelChanged } from "./connect-telemetry.js";
|
|
48
|
+
import { noteChipDraft, noteDraftConsumed, noteDraftInput, stagePrompt } from "./prompt-telemetry.js";
|
|
47
49
|
|
|
48
50
|
function relativeTime(value, now = Date.now()) {
|
|
49
51
|
const at = Date.parse(value || "");
|
|
@@ -243,7 +245,7 @@ function FailureCard({ failure, onRetry, onFixKey }) {
|
|
|
243
245
|
<div className="agent-failure-head"><FiAlertTriangle size={12} aria-hidden="true" /><span className="agent-failure-code">{failure.code}</span></div>
|
|
244
246
|
<p className="agent-failure-message">{failure.message || "The command did not complete."}</p>
|
|
245
247
|
<p className="agent-failure-recovery">{RECOVERY_COPY[failure.recovery?.action] || RECOVERY_COPY.none}{failure.preserved?.authoredState === "unchanged" ? " Nothing in the scene changed." : failure.preserved?.authoredState === "unknown" ? " Whether the scene changed is unknown." : ""}</p>
|
|
246
|
-
{failure.recovery?.retryAllowed && <div className="agent-error-actions">{failure.recovery.action === "fix_key" && onFixKey && <button type="button" className="agent-ghost-button agent-error-keys" onClick={onFixKey}>Log in again</button>}<button type="button" className="agent-ghost-button agent-error-retry" onClick={onRetry}>Retry</button></div>}
|
|
248
|
+
{failure.recovery?.retryAllowed && <div className="agent-error-actions">{failure.recovery.action === "fix_key" && onFixKey && <button type="button" className="agent-ghost-button agent-error-keys" onClick={onFixKey}>Log in again</button>}<button type="button" className="agent-ghost-button agent-error-retry" data-fid="agent_retry" onClick={onRetry}>Retry</button></div>}
|
|
247
249
|
</div>;
|
|
248
250
|
}
|
|
249
251
|
|
|
@@ -311,7 +313,7 @@ export default function AgentPanel({
|
|
|
311
313
|
// Picking a model is a preference, not a per-session decision: the key is
|
|
312
314
|
// remembered so a reload does not quietly move the next turn to another
|
|
313
315
|
// provider's model.
|
|
314
|
-
const chooseModel = useCallback((id) => { setModel(id); setEffort(null); storeModel(id); }, []);
|
|
316
|
+
const chooseModel = useCallback((id) => { setModel(id); setEffort(null); storeModel(id); modelChanged(id); }, []);
|
|
315
317
|
// Task mode (parallel Studio tasks): the chat talks to the main agent, which answers at once and runs tasks on the board.
|
|
316
318
|
const [taskMode, setTaskMode] = useState(() => surface === "studio" && readStoredTaskMode());
|
|
317
319
|
const toggleTaskMode = useCallback(() => setTaskMode((current) => { storeTaskMode(!current); return !current; }), []);
|
|
@@ -639,9 +641,10 @@ export default function AgentPanel({
|
|
|
639
641
|
if (!event.defaultPrevented && takeTransfer(event.dataTransfer)) event.preventDefault();
|
|
640
642
|
}, [takeTransfer]);
|
|
641
643
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
setSteerNotice("");
|
|
644
|
+
// `via`: how the prompt was sent, for agent:prompt_sent (prompt-telemetry.js keeps no text).
|
|
645
|
+
const runTurn = useCallback((text, via = "typed") => {
|
|
646
|
+
setAttachNotice(null); setSteerNotice("");
|
|
647
|
+
stagePrompt(text, { via, model, attached: attachFrame || store.getState().pendingAttachments.length > 0, taskMode });
|
|
645
648
|
return store.send(text, { attachFrame, model, effort: effort ?? undefined, tasks: taskMode });
|
|
646
649
|
}, [attachFrame, effort, model, store, taskMode]);
|
|
647
650
|
const taskStepLabel = useCallback((call) => stepLabel(call, presentation.toolLabels), [presentation.toolLabels]);
|
|
@@ -652,10 +655,10 @@ export default function AgentPanel({
|
|
|
652
655
|
// leaves the draft to be sent as a normal message.
|
|
653
656
|
const steerTurn = useCallback(async (text) => {
|
|
654
657
|
if (!String(text ?? "").trim()) return;
|
|
655
|
-
setAttachNotice(null);
|
|
656
|
-
setSteerNotice("");
|
|
658
|
+
setAttachNotice(null); setSteerNotice("");
|
|
657
659
|
const result = await store.steer(text);
|
|
658
|
-
|
|
660
|
+
// An accepted steer consumed the draft: its typing time is not the next prompt's (prompt-telemetry.js).
|
|
661
|
+
if (result.ok) noteDraftConsumed(); else setSteerNotice(result.message || "The running turn did not take that message.");
|
|
659
662
|
}, [store]);
|
|
660
663
|
|
|
661
664
|
// A sign-in that succeeds is an authentication transition, not a status
|
|
@@ -664,13 +667,13 @@ export default function AgentPanel({
|
|
|
664
667
|
// that asked for them. Refreshing the account alone leaves the panel
|
|
665
668
|
// "ready" with nothing it may send (#379).
|
|
666
669
|
const signIn = useCallback(async () => {
|
|
667
|
-
setAuthState("signing-in");
|
|
670
|
+
connectStarted("openai-codex", "chatgpt"); setAuthState("signing-in");
|
|
668
671
|
setSignInError("");
|
|
669
672
|
try {
|
|
670
673
|
await transport.signIn();
|
|
671
674
|
await refreshProviderState();
|
|
672
675
|
} catch (error) {
|
|
673
|
-
if (agentServerUnavailable(error)) { setAuthState("offline"); return; }
|
|
676
|
+
connectFinished("failed"); if (agentServerUnavailable(error)) { setAuthState("offline"); return; }
|
|
674
677
|
setSignInError(error?.message || ko("Sign-in could not start.", "로그인을 시작하지 못했어요."));
|
|
675
678
|
setAuthState("signed-out");
|
|
676
679
|
}
|
|
@@ -718,15 +721,11 @@ export default function AgentPanel({
|
|
|
718
721
|
|
|
719
722
|
// The section is read when it opens; every write goes back through the
|
|
720
723
|
// refresh above, because a key changes the session, not just a row.
|
|
721
|
-
const toggleProviderKeys = useCallback(() => {
|
|
722
|
-
setMenuOpen(false);
|
|
723
|
-
setKeysOpen(!keysOpen);
|
|
724
|
-
}, [keysOpen]);
|
|
724
|
+
const toggleProviderKeys = useCallback(() => { setMenuOpen(false); if (!keysOpen) connectOpened("menu"); setKeysOpen(!keysOpen); }, [keysOpen]);
|
|
725
725
|
// Opens (never toggles) Connect a model, pi's login list: the signed-out card and a refused key both lead here.
|
|
726
|
-
const openProviderKeys = useCallback(() => {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}, []);
|
|
726
|
+
const openProviderKeys = useCallback((entry) => { setMenuOpen(false); connectOpened(entry); setKeysOpen(true); }, []);
|
|
727
|
+
// The connect funnel's ChatGPT attempt settles with the auth state (connect-telemetry.js).
|
|
728
|
+
useEffect(() => { connectAuthState(authState, account); }, [authState, account]);
|
|
730
729
|
|
|
731
730
|
// Clearing the transcript also retires the session, so what the author sees
|
|
732
731
|
// and what the model remembers cannot diverge.
|
|
@@ -842,7 +841,7 @@ export default function AgentPanel({
|
|
|
842
841
|
{resetLabel && <span className="agent-account-reset">resets in {resetLabel}</span>}
|
|
843
842
|
</div>}
|
|
844
843
|
<button type="button" role="menuitem" onClick={clearContext}>Clear context</button>
|
|
845
|
-
<button type="button" role="menuitem" className="agent-menu-keys" aria-expanded={keysOpen} onClick={toggleProviderKeys}
|
|
844
|
+
<button type="button" role="menuitem" className="agent-menu-keys" data-fid="agent_connect" aria-expanded={keysOpen} onClick={toggleProviderKeys}
|
|
846
845
|
disabled={authState === "offline"} title={authState === "offline" ? ko("The agent server is not running", "에이전트 서버가 실행 중이 아니에요") : undefined}>Connect a model…</button>
|
|
847
846
|
{account?.signedIn && <button type="button" role="menuitem" onClick={signOut}>Sign out</button>}
|
|
848
847
|
</div>}
|
|
@@ -863,7 +862,7 @@ export default function AgentPanel({
|
|
|
863
862
|
<p>Sign in with ChatGPT, or log in to another provider with a subscription, an API key, or your own proxy server. Sign-in happens in your browser — no token is ever stored in this page.</p>
|
|
864
863
|
<button type="button" className="agent-primary-button agent-signin" onClick={signIn}>Sign in with ChatGPT</button>
|
|
865
864
|
{signInError && <p className="agent-key-error agent-signin-error" role="alert">{signInError}</p>}
|
|
866
|
-
<button type="button" className="agent-ghost-button agent-use-key" onClick={openProviderKeys}>Log in with another provider</button>
|
|
865
|
+
<button type="button" className="agent-ghost-button agent-use-key" onClick={() => openProviderKeys("signed_out")}>Log in with another provider</button>
|
|
867
866
|
</div>}
|
|
868
867
|
|
|
869
868
|
{authState === "offline" && <OfflineCard probing={probing} onRetry={retryServer} />}
|
|
@@ -885,7 +884,7 @@ export default function AgentPanel({
|
|
|
885
884
|
<h3>{presentation.emptyTitle}</h3>
|
|
886
885
|
<p>{presentation.emptyHint(sceneName)}</p>
|
|
887
886
|
<div className="agent-suggestions">
|
|
888
|
-
{presentation.suggestions.map((chip) => <button type="button" key={chip} className="agent-chip" onClick={() => { store.setDraft(chip); composerRef.current?.focus(); }}>{chip}</button>)}
|
|
887
|
+
{presentation.suggestions.map((chip) => <button type="button" key={chip} className="agent-chip" onClick={() => { noteChipDraft(); store.setDraft(chip); composerRef.current?.focus(); }}>{chip}</button>)}
|
|
889
888
|
</div>
|
|
890
889
|
</div>}
|
|
891
890
|
|
|
@@ -893,7 +892,7 @@ export default function AgentPanel({
|
|
|
893
892
|
{layout.map((entry, index) => {
|
|
894
893
|
if (entry.type === "steps") return <Fragment key={entry.id}>
|
|
895
894
|
{entry.last && progressCard}
|
|
896
|
-
<div className="agent-row"><TurnSteps block={entry} now={activityNow} toolLabels={presentation.toolLabels} formatClock={formatTurnClock} onRetry={() => runTurn(chat.lastPrompt)} /></div>{taskMode && layout[index + 1]?.item?.kind !== "assistant" && <TaskRows steps={entry.steps} />}
|
|
895
|
+
<div className="agent-row"><TurnSteps block={entry} now={activityNow} toolLabels={presentation.toolLabels} formatClock={formatTurnClock} onRetry={() => runTurn(chat.lastPrompt, "retry_button")} /></div>{taskMode && layout[index + 1]?.item?.kind !== "assistant" && <TaskRows steps={entry.steps} />}
|
|
897
896
|
</Fragment>;
|
|
898
897
|
const { item } = entry;
|
|
899
898
|
if (item.kind === "user") return <div className="agent-row user" key={item.id}>
|
|
@@ -905,14 +904,14 @@ export default function AgentPanel({
|
|
|
905
904
|
if (item.kind === "assistant") return <Fragment key={item.id}><div className="agent-row assistant"><div className="agent-assistant-text"><ReplyText text={item.text} tail={streaming && <span className="agent-caret">▌</span>} /></div></div>{taskMode && layout[index - 1]?.type === "steps" && <TaskRows steps={layout[index - 1].steps} />}</Fragment>;
|
|
906
905
|
if (item.kind === "job") return <div className="agent-row" key={item.id}><JobCard job={item} onStop={stopTurn} onAccept={(job) => store.acceptJob(job.jobId)} /></div>;
|
|
907
906
|
if (item.kind === "receipt") return <div className="agent-row" key={item.id}><ReceiptCard item={item} /></div>;
|
|
908
|
-
if (item.kind === "failure") return <div className="agent-row" key={item.id}><FailureCard failure={item.failure} onRetry={() => runTurn(chat.lastPrompt)} onFixKey={openProviderKeys} /></div>;
|
|
909
|
-
if (item.kind === "supervisor") return <div className="agent-row" key={item.id}><SupervisorCard note={item} onSend={(text) => { store.setDraft(text); runTurn(text); }} /></div>;
|
|
907
|
+
if (item.kind === "failure") return <div className="agent-row" key={item.id}><FailureCard failure={item.failure} onRetry={() => runTurn(chat.lastPrompt, "retry_button")} onFixKey={() => openProviderKeys("failure_card")} /></div>;
|
|
908
|
+
if (item.kind === "supervisor") return <div className="agent-row" key={item.id}><SupervisorCard note={item} onSend={(text) => { store.setDraft(text); runTurn(text, "supervisor"); }} /></div>;
|
|
910
909
|
if (item.kind === "plan") return <PlanCard key={item.id} plan={item} live={streaming && !item.outcome} />;
|
|
911
|
-
return <div className="agent-row" key={item.id}><ImageResultCard image={item} onUse={(image) => store.applyImage(image.id)} onUndo={(image) => store.undoImage(image.id)} onRegenerate={() => runTurn(chat.lastPrompt)} onOpen={setLightbox} /></div>;
|
|
910
|
+
return <div className="agent-row" key={item.id}><ImageResultCard image={item} onUse={(image) => store.applyImage(image.id)} onUndo={(image) => store.undoImage(image.id)} onRegenerate={() => runTurn(chat.lastPrompt, "regenerate")} onOpen={setLightbox} /></div>;
|
|
912
911
|
})}
|
|
913
912
|
|
|
914
913
|
{!layout.some((entry) => entry.type === "steps" && entry.last) && progressCard}{taskMode && <TaskRows items={items} />}
|
|
915
|
-
{rateLimit && <PausedCard resetAt={rateLimit.resetAt} onRetry={() => { store.clearRateLimit(); runTurn(chat.lastPrompt); }} onSwitchModel={switchModel} />}
|
|
914
|
+
{rateLimit && <PausedCard resetAt={rateLimit.resetAt} onRetry={() => { store.clearRateLimit(); runTurn(chat.lastPrompt, "retry_button"); }} onSwitchModel={switchModel} />}
|
|
916
915
|
</div>
|
|
917
916
|
{surface === "studio" && taskMode && authenticated && <><Suspense fallback={null}><TaskBoard sessionId={chat.sessionId} pulse={chat.lastTurn?.endedAt ?? 0} labelOf={taskStepLabel} onMainNote={store.note} /></Suspense><TaskDetail /></>}
|
|
918
917
|
|
|
@@ -947,7 +946,7 @@ export default function AgentPanel({
|
|
|
947
946
|
placeholder={panelState === "rate-limited" ? "Composer is paused" : model ? presentation.composerPlaceholder : "Waiting for the model list…"}
|
|
948
947
|
value={draft}
|
|
949
948
|
disabled={composerDisabled}
|
|
950
|
-
onChange={(event) => store.setDraft(event.target.value)}
|
|
949
|
+
onChange={(event) => { noteDraftInput(); store.setDraft(event.target.value); }}
|
|
951
950
|
onKeyDown={onComposerKeyDown}
|
|
952
951
|
onDragOver={onComposerDragOver}
|
|
953
952
|
onDrop={onComposerDrop}
|
|
@@ -991,10 +990,10 @@ export default function AgentPanel({
|
|
|
991
990
|
that can be steered, Send becomes the way INTO it. */}
|
|
992
991
|
{streaming
|
|
993
992
|
? <>
|
|
994
|
-
<button type="button" className="agent-send stop agent-stop" onClick={stopTurn}>Stop</button>
|
|
995
|
-
{presentation.steer && <button type="button" className="agent-send agent-steer" disabled={!draft.trim()} onClick={() => steerTurn(draft)}>Steer</button>}
|
|
993
|
+
<button type="button" data-fid="agent_stop" className="agent-send stop agent-stop" onClick={stopTurn}>Stop</button>
|
|
994
|
+
{presentation.steer && <button type="button" data-fid="agent_steer" className="agent-send agent-steer" disabled={!draft.trim()} onClick={() => steerTurn(draft)}>Steer</button>}
|
|
996
995
|
</>
|
|
997
|
-
: <button type="button" className="agent-send" aria-label="Send" title="Send (Enter)" disabled={composerDisabled || !draft.trim()} onClick={() => runTurn(draft)}><FiArrowUp size={14} aria-hidden="true" /></button>}
|
|
996
|
+
: <button type="button" data-fid="agent_send" className="agent-send" aria-label="Send" title="Send (Enter)" disabled={composerDisabled || !draft.trim()} onClick={() => runTurn(draft)}><FiArrowUp size={14} aria-hidden="true" /></button>}
|
|
998
997
|
</div>
|
|
999
998
|
<ModelRolesLine model={model} roles={selectedModelRoles(modelRoles, model)} />
|
|
1000
999
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { track } from "../analytics.js"; import { startExportAttempt } from "../export-attempt.js";
|
|
3
3
|
import { downloadPack, requestKeyframePack } from "./keyframe-pack-request.js";
|
|
4
4
|
import {
|
|
5
5
|
applyCozyScenePatch,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { FiX } from "react-icons/fi";
|
|
3
3
|
import { answerLogin, cancelLogin, fetchLogins, logoutProvider, readLogin, startLogin } from "./login-client.js";
|
|
4
|
+
import { connectFinished, connectPanelClosed, connectStarted, connectStep } from "./connect-telemetry.js";
|
|
4
5
|
|
|
5
6
|
// "Connect a model": pi's own login (the coding agent's /login) inside the agent panel (#pi-login). The list
|
|
6
7
|
// is what pi ships: each provider with its subscription sign-in and/or api-key login; picking one runs pi's
|
|
@@ -53,7 +54,14 @@ export function LoginPanel({ onClose, onChatGPT, onChanged }) {
|
|
|
53
54
|
const load = useCallback(async () => {
|
|
54
55
|
try { setProviders(await fetchLogins()); setError(""); } catch (failure) { setError(failure?.message || "The login list is unavailable."); }
|
|
55
56
|
}, []);
|
|
56
|
-
|
|
57
|
+
// Leaving the panel mid-flow abandons that attempt (a ChatGPT sign-in goes on in the agent panel).
|
|
58
|
+
useEffect(() => { live.current = true; load(); return () => { live.current = false; connectPanelClosed(); }; }, [load]);
|
|
59
|
+
// The attempt's funnel step follows the flow: waiting, a prompt to answer, then done.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!flow?.flowId) return;
|
|
62
|
+
if (flow.done) connectFinished(flow.ok ? "ok" : "failed");
|
|
63
|
+
else connectStep(flow.prompt ? "prompt" : "waiting");
|
|
64
|
+
}, [flow?.flowId, flow?.done, flow?.ok, flow?.prompt]);
|
|
57
65
|
// Read the running flow until pi finishes it.
|
|
58
66
|
useEffect(() => {
|
|
59
67
|
if (!flow?.flowId || flow.done) return undefined;
|
|
@@ -83,12 +91,14 @@ export function LoginPanel({ onClose, onChatGPT, onChanged }) {
|
|
|
83
91
|
|
|
84
92
|
const begin = async (provider, method) => {
|
|
85
93
|
setError("");
|
|
86
|
-
// ChatGPT's own sign-in shows its waiting card in the panel body, so the list steps aside
|
|
94
|
+
// ChatGPT's own sign-in shows its waiting card in the panel body, so the list steps aside;
|
|
95
|
+
// that sign-in reports its own connect attempt (AgentPanel signIn).
|
|
87
96
|
if (method.type === "chatgpt") { onChatGPT?.(); onClose?.(); return; }
|
|
97
|
+
connectStarted(provider.id, method.type);
|
|
88
98
|
try {
|
|
89
99
|
const flowId = await startLogin(provider.id, method.type);
|
|
90
100
|
setFlow({ flowId, name: provider.name, label: method.label, log: [], prompt: null, done: false });
|
|
91
|
-
} catch (failure) { setError(failure?.message || "That login could not start."); }
|
|
101
|
+
} catch (failure) { connectFinished("failed"); setError(failure?.message || "That login could not start."); }
|
|
92
102
|
};
|
|
93
103
|
const answer = async (value) => {
|
|
94
104
|
const prompt = flow?.prompt;
|
|
@@ -97,7 +107,7 @@ export function LoginPanel({ onClose, onChatGPT, onChanged }) {
|
|
|
97
107
|
try { await answerLogin(flow.flowId, prompt.promptId, value); } catch (failure) { setError(failure?.message || "That answer was not taken."); }
|
|
98
108
|
};
|
|
99
109
|
const cancel = async () => {
|
|
100
|
-
if (flow && !flow.done) await cancelLogin(flow.flowId).catch(() => {});
|
|
110
|
+
if (flow && !flow.done) { connectFinished("cancelled"); await cancelLogin(flow.flowId).catch(() => {}); }
|
|
101
111
|
setFlow(null);
|
|
102
112
|
};
|
|
103
113
|
const logout = async (provider) => {
|
|
@@ -108,7 +118,7 @@ export function LoginPanel({ onClose, onChatGPT, onChanged }) {
|
|
|
108
118
|
return <section className="agent-keys agent-login" aria-label="Connect a model" data-agent-keys="true">
|
|
109
119
|
<div className="agent-keys-head">
|
|
110
120
|
<h3 className="agent-keys-title">Connect a model</h3>
|
|
111
|
-
<button type="button" className="agent-icon-button agent-keys-close" aria-label="Close" onClick={async () => { await cancel(); onClose?.(); }}><FiX size={11} aria-hidden="true" /></button>
|
|
121
|
+
<button type="button" className="agent-icon-button agent-keys-close" aria-label="Close" onClick={async () => { connectFinished("abandoned"); await cancel(); onClose?.(); }}><FiX size={11} aria-hidden="true" /></button>
|
|
112
122
|
</div>
|
|
113
123
|
{error && <p className="agent-key-error" role="alert">{error}</p>}
|
|
114
124
|
{flow ? <div className="agent-login-flow" data-login-flow={flow.done ? (flow.ok ? "connected" : "failed") : "running"}>
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
// POST /agent/jobs/<jobId>/accept -> explicit "Apply with warnings"
|
|
24
24
|
// GET /agent/turn/<turnId>/supervisor -> 200 supervisor note | 204 none (#728)
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { track } from "../analytics.js";
|
|
27
27
|
import { appendAttachments, ATTACHMENT_MAX_COUNT } from "./attachment-image.js";
|
|
28
28
|
import { ko } from "../locale.js";
|
|
29
|
-
import {
|
|
29
|
+
import { startAgentTurn, trackAgentTurns } from "./agent-turn-telemetry.js";
|
|
30
30
|
import { STUDIO_VARIANTS, validateReceipt } from "../studio-agent-protocol.js";
|
|
31
|
+
import { refusalFailureCode, transportFailureCode } from "./agent-failure-codes.js";
|
|
31
32
|
import { requestAgentImage, sidecarRequest, sidecarUrl } from "../agent-image-client.js"; import { clearSculptSketch, sculptSketchFrame } from "../sculpt-sketch-state.js";
|
|
32
33
|
import { actionLabelsOf, changedLine, scriptNow, stepFields, thinkingLine, withCompose, withThinking, withToolStart, withTurnEnd } from "./agent-turn.js";
|
|
33
34
|
import { isPlanFrame, restoredPlans, withPlanEnd, withPlanFrame } from "./agent-plan.js";
|
|
@@ -489,13 +490,7 @@ export function parseSseChunk(buffer) {
|
|
|
489
490
|
return { events, tail };
|
|
490
491
|
}
|
|
491
492
|
|
|
492
|
-
const validTelemetryId = (value) => typeof value === "string" && /^[a-f0-9]{32}$/.test(value);
|
|
493
|
-
const AGENT_FAILURE_CODES = new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "unknown"]);
|
|
494
|
-
const exactKeys = (value, keys) => value && typeof value === "object" && !Array.isArray(value)
|
|
495
|
-
&& Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
496
493
|
const advisory = (read, fallback) => { try { return read(); } catch { return fallback; } };
|
|
497
|
-
const transportFailureCode = (error) => advisory(() => error?.status === 401 || error?.code === "auth" ? "auth"
|
|
498
|
-
: error?.status === 429 || error?.code === "rate_limit" ? "rate_limited" : "upstream", "upstream");
|
|
499
494
|
|
|
500
495
|
const UI_ERROR_CODES = new Set(["auth", "entitlement", "rate_limit", "overloaded", "upstream"]);
|
|
501
496
|
|
|
@@ -523,86 +518,6 @@ export async function refusalEvent(response) {
|
|
|
523
518
|
};
|
|
524
519
|
}
|
|
525
520
|
|
|
526
|
-
// The real browser request owns the attempt, including HTTP refusal and Stop.
|
|
527
|
-
// These IDs never use panel session IDs, model call IDs or authored content.
|
|
528
|
-
function startAgentTurn({ surface, capture, now, correlationId = null }) {
|
|
529
|
-
const turnId = advisory(() => {
|
|
530
|
-
const bytes = new Uint8Array(16);
|
|
531
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
532
|
-
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
533
|
-
}, null);
|
|
534
|
-
const clock = () => advisory(() => { const value = now(); return Number.isFinite(value) ? value : NaN; }, NaN);
|
|
535
|
-
// A Studio turn already has a host-owned id and its frozen envelope has no
|
|
536
|
-
// room for a second one, so the sidecar names its frames with THAT id. The
|
|
537
|
-
// analytics id stays the local hex one: it is the only id analytics accepts.
|
|
538
|
-
const matchId = correlationId ?? turnId;
|
|
539
|
-
const startedAt = clock();
|
|
540
|
-
const hostSurface = advisory(() => ["studio", "workflow"].includes(surface) ? surface
|
|
541
|
-
: /^\/workflow(?:\/|$)/.test(globalThis.location?.pathname || "") ? "workflow" : "studio", "studio");
|
|
542
|
-
let terminal = false;
|
|
543
|
-
let applied = false;
|
|
544
|
-
const tools = new Map();
|
|
545
|
-
const seenTools = new Set();
|
|
546
|
-
const emit = (event, props) => {
|
|
547
|
-
if (!turnId) return;
|
|
548
|
-
try { Promise.resolve(capture(event, props)).catch(() => {}); } catch { /* telemetry is advisory */ }
|
|
549
|
-
};
|
|
550
|
-
const finish = (outcome, failureCode) => {
|
|
551
|
-
if (terminal) return;
|
|
552
|
-
terminal = true;
|
|
553
|
-
if (outcome === "cancelled") {
|
|
554
|
-
for (const [id, tool] of tools) {
|
|
555
|
-
if (!seenTools.has(id)) emit("agent:tool_executed", { turn_id: turnId, tool_category: tool.category, outcome: "cancelled", duration_bucket: bucketMs(clock() - tool.startedAt) });
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
tools.clear();
|
|
559
|
-
emit(`agent:turn_${outcome}`, { turn_id: turnId, duration_bucket: bucketMs(clock() - startedAt), ...(outcome !== "succeeded" ? { failure_code: failureCode } : {}) });
|
|
560
|
-
};
|
|
561
|
-
emit("agent:turn_requested", { surface: hostSurface, turn_id: turnId });
|
|
562
|
-
return {
|
|
563
|
-
turnId,
|
|
564
|
-
cancel: () => finish("cancelled", "aborted"),
|
|
565
|
-
fail: (code) => finish("failed", AGENT_FAILURE_CODES.has(code) ? code : "upstream"),
|
|
566
|
-
frame(frame) {
|
|
567
|
-
// Local sidecar frames are still an untrusted boundary. Reject the whole
|
|
568
|
-
// frame, including extra fields, before dedupe or analytics capture.
|
|
569
|
-
try {
|
|
570
|
-
if (!turnId || terminal) return;
|
|
571
|
-
if (frame.type === "execution_tool_started") {
|
|
572
|
-
if (!exactKeys(frame, ["type", "turn_id", "telemetry_id", "tool_category"]) || frame.turn_id !== matchId
|
|
573
|
-
|| !validTelemetryId(frame.telemetry_id) || !AGENT_TOOL_CATEGORIES.includes(frame.tool_category)) return;
|
|
574
|
-
if (!tools.has(frame.telemetry_id) && !seenTools.has(frame.telemetry_id)) tools.set(frame.telemetry_id, { category: frame.tool_category, startedAt: clock() });
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
const { event, props } = frame;
|
|
578
|
-
if (props?.turn_id !== matchId) return;
|
|
579
|
-
if (event === "agent:tool_executed") {
|
|
580
|
-
if (!exactKeys(frame, ["type", "event", "props", "telemetry_id"]) || !validTelemetryId(frame.telemetry_id)
|
|
581
|
-
|| !exactKeys(props, ["turn_id", "tool_category", "outcome", "duration_bucket"])
|
|
582
|
-
|| !AGENT_TOOL_CATEGORIES.includes(props.tool_category) || !["succeeded", "failed", "cancelled"].includes(props.outcome)
|
|
583
|
-
|| !EXECUTION_TELEMETRY_VALUES.duration_bucket.has(props.duration_bucket) || seenTools.has(frame.telemetry_id)) return;
|
|
584
|
-
seenTools.add(frame.telemetry_id);
|
|
585
|
-
tools.delete(frame.telemetry_id);
|
|
586
|
-
emit(event, { turn_id: turnId, tool_category: props.tool_category, outcome: props.outcome, duration_bucket: props.duration_bucket });
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
if (!exactKeys(frame, ["type", "event", "props"])) return;
|
|
590
|
-
if (event === "agent:result_applied") {
|
|
591
|
-
if (!applied && exactKeys(props, ["turn_id"])) { applied = true; emit(event, { turn_id: turnId }); }
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
if (!["agent:turn_succeeded", "agent:turn_failed", "agent:turn_cancelled"].includes(event)) return;
|
|
595
|
-
const outcome = event.slice("agent:turn_".length);
|
|
596
|
-
const hasFailure = Object.hasOwn(props, "failure_code");
|
|
597
|
-
if (!exactKeys(props, ["turn_id", "duration_bucket", ...(hasFailure ? ["failure_code"] : [])])
|
|
598
|
-
|| !EXECUTION_TELEMETRY_VALUES.duration_bucket.has(props.duration_bucket)
|
|
599
|
-
|| (hasFailure && (outcome === "succeeded" || !AGENT_FAILURE_CODES.has(props.failure_code)))) return;
|
|
600
|
-
finish(outcome, outcome === "cancelled" ? "aborted" : props.failure_code || "unknown");
|
|
601
|
-
} catch { /* foreign getters and malformed telemetry cannot affect a turn */ }
|
|
602
|
-
},
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
|
|
606
521
|
const RESUME_ATTEMPTS = 3;
|
|
607
522
|
|
|
608
523
|
/** The environment variables the sidecar reads a provider key from (the `env`
|
|
@@ -732,7 +647,10 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
|
|
|
732
647
|
const attached = Array.isArray(attachments) && attachments.length ? { attachments } : {};
|
|
733
648
|
const studio = turnRequest.surface === "studio";
|
|
734
649
|
const turnId = studio ? turnRequest.turnId : null;
|
|
735
|
-
|
|
650
|
+
// The host's own surface wins over the URL (an embedded Studio panel on a /workflow path is still Studio).
|
|
651
|
+
const hostSurface = advisory(() => ["studio", "workflow"].includes(surface) ? surface
|
|
652
|
+
: /^\/workflow(?:\/|$)/.test(globalThis.location?.pathname || "") ? "workflow" : "studio", "studio");
|
|
653
|
+
const telemetry = startAgentTurn({ surface: hostSurface, capture, now, correlationId: turnId });
|
|
736
654
|
// The steer route is keyed by the id THIS request is known by upstream:
|
|
737
655
|
// the Studio envelope's turnId, or the hex id the dock mints for its own
|
|
738
656
|
// turn. The caller cannot know it any other way.
|
|
@@ -755,6 +673,7 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
|
|
|
755
673
|
}
|
|
756
674
|
if (event?.type === "error") failureCode = transportFailureCode(event);
|
|
757
675
|
if (event?.type === "done") terminal = true;
|
|
676
|
+
if (event?.type === "tool.start" || event?.type === "tool.done") telemetry.tool(event);
|
|
758
677
|
onEvent(event);
|
|
759
678
|
};
|
|
760
679
|
const readStream = async (response) => {
|
|
@@ -795,7 +714,7 @@ export function createHttpTransport({ fetchImpl = globalThis.fetch?.bind(globalT
|
|
|
795
714
|
signal,
|
|
796
715
|
});
|
|
797
716
|
if (!response.ok || !response.body) {
|
|
798
|
-
telemetry.fail(
|
|
717
|
+
telemetry.fail(await refusalFailureCode(response));
|
|
799
718
|
// A refused turn carries the only explanation there is. Forward the
|
|
800
719
|
// status and whatever short detail the sidecar sanitized for us
|
|
801
720
|
// instead of the panel inventing "something went wrong".
|
|
@@ -1134,7 +1053,7 @@ export function createAgentTransport(options = {}) {
|
|
|
1134
1053
|
const config = options.mockConfig !== undefined ? options.mockConfig : mockConfigFromSearch();
|
|
1135
1054
|
// The surface is the host's own prop, never a guess from the URL: an
|
|
1136
1055
|
// embedded Studio panel is a Studio panel wherever it is mounted.
|
|
1137
|
-
return config ? createMockTransport({ surface: options.surface, ...config }) : createHttpTransport(options);
|
|
1056
|
+
return trackAgentTurns(config ? createMockTransport({ surface: options.surface, ...config }) : createHttpTransport(options));
|
|
1138
1057
|
}
|
|
1139
1058
|
|
|
1140
1059
|
// One conversation: session identity, transcript, motion jobs, receipts and
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The closed `agent:turn_failed.failure_code` vocabulary, browser side. The
|
|
2
|
+
// sidecar keeps an identical table in bin/agent/agent-failure-codes.mjs (it
|
|
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, never a message.
|
|
5
|
+
//
|
|
6
|
+
// key_refused the model provider rejected the credentials (provider
|
|
7
|
+
// 401/403, structured or message-only: runner `unauthorized`)
|
|
8
|
+
// auth the sidecar itself has no usable sign-in, or refused the
|
|
9
|
+
// session's ownership (AUTH_REQUIRED, `auth`, refusal 401/403)
|
|
10
|
+
// rate_limited provider or sidecar 429 (`rate_limit`, RATE_LIMITED)
|
|
11
|
+
// stale_scene the scene moved under the turn (STALE_SCENE/TARGET/ENVIRONMENT)
|
|
12
|
+
// capability_missing the sidecar cannot run this turn here (CAPABILITY_MISSING:
|
|
13
|
+
// Studio runtime absent, live hub port in use;
|
|
14
|
+
// LIVE_HUB_UNAVAILABLE: no connected editor)
|
|
15
|
+
// busy a turn is already running, or the target is busy
|
|
16
|
+
// invalid_request the sidecar rejected the request (400, INVALID_*, other 4xx)
|
|
17
|
+
// upstream provider outage, overload, 5xx, network or unclassified
|
|
18
|
+
// aborted / tool_failed / unknown are assigned by the turn, not by this table
|
|
19
|
+
export const AGENT_FAILURE_CODES = new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "unknown",
|
|
20
|
+
"key_refused", "stale_scene", "capability_missing", "busy", "invalid_request"]);
|
|
21
|
+
|
|
22
|
+
const BY_CODE = {
|
|
23
|
+
unauthorized: "key_refused", auth: "auth", AUTH_REQUIRED: "auth", rate_limit: "rate_limited", RATE_LIMITED: "rate_limited",
|
|
24
|
+
STALE_SCENE: "stale_scene", STALE_TARGET: "stale_scene", STALE_ENVIRONMENT: "stale_scene",
|
|
25
|
+
CAPABILITY_MISSING: "capability_missing", LIVE_HUB_UNAVAILABLE: "capability_missing",
|
|
26
|
+
busy: "busy", TARGET_BUSY: "busy", SCOPE_BUSY: "busy", TARGET_NOT_READY: "busy",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** `{ code, status }` -> failure code. Foreign getters fall back to upstream. */
|
|
30
|
+
export function transportFailureCode(error) {
|
|
31
|
+
try {
|
|
32
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
33
|
+
const status = error?.status;
|
|
34
|
+
if (Object.hasOwn(BY_CODE, code)) return BY_CODE[code];
|
|
35
|
+
if (code.startsWith("INVALID_")) return "invalid_request";
|
|
36
|
+
if (status === 401 || status === 403) return "auth";
|
|
37
|
+
if (status === 429) return "rate_limited";
|
|
38
|
+
return Number.isInteger(status) && status >= 400 && status < 500 ? "invalid_request" : "upstream";
|
|
39
|
+
} catch { return "upstream"; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A refused turn: the sidecar's `{ error: { code } }` body plus the HTTP status. */
|
|
43
|
+
export async function refusalFailureCode(response) {
|
|
44
|
+
let code;
|
|
45
|
+
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 "upstream"; }
|
|
47
|
+
}
|