Keep creation as a living DAG and archive playable products separately.

Opening accept no longer locks the graph; session.json stays creation-only while play starts from an instance snapshot. Sidebar products nest run saves, and play reading fills the feed at a denser type size.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 00:52:05 +08:00
co-authored by Cursor
parent bd5d88abfa
commit f37388761b
22 changed files with 930 additions and 303 deletions
+65 -13
View File
@@ -76,6 +76,34 @@ function isPlayView(view) {
return Boolean(view?.playLayerActive || view?.lifecycleStage === "play");
}
/** 开场已选定、创作收口,仍停在创作层等用户保存产物 */
export function isCreationSealedWaiting(view) {
if (!view || isPlayView(view)) return false;
const msg = view.waitingReason?.message;
return (
view.waitingReason?.kind === "input" &&
typeof msg === "string" &&
/创作已收口|可保存为产物/.test(msg)
);
}
/** 收口节点已验收(图上「已完成」),即使还停在选节点 */
export function isCreationCloserDone(view) {
if (!view || isPlayView(view)) return false;
const steps = view.creationFlowView?.steps;
if (!Array.isArray(steps)) return false;
return steps.some((s) => s?.closer && s.runState === "done");
}
export function canOfferSaveProduct(view) {
return isCreationSealedWaiting(view) || isCreationCloserDone(view);
}
export function canOfferEnterPlay(view) {
if (!view || isPlayView(view)) return false;
return canOfferSaveProduct(view) || Boolean(view.hasProduct);
}
function renderLiveStreamBody(live, view) {
const thinking = (live?.thinking || view?.agentThinking || "").trim();
const output = (live?.output ?? "").trim();
@@ -848,16 +876,19 @@ export function renderLifecycle(view) {
document.body.dataset.lifecycle = stage;
const toggle = document.getElementById("lifecycle-toggle");
if (!toggle) return;
const ready = Boolean(view?.playReady || view?.hasProduct);
toggle.hidden = !ready;
toggle.querySelectorAll("[data-stage]").forEach((btn) => {
const s = btn.getAttribute("data-stage");
btn.classList.toggle("active", s === stage);
if (s === "play") {
btn.disabled = !view.playReady;
btn.title = view.playReady
? "进入游玩 / 写作"
: "请先验收 Worker 集(创作定稿)后再切换";
btn.disabled = !ready;
btn.title = ready
? "用已保存的产物开玩"
: "请先保存产物后再开玩";
} else {
btn.disabled = false;
btn.title = "回到创作";
}
});
}
@@ -4312,7 +4343,12 @@ function renderSpeakWorkspace(view) {
let hint = "在底栏输入你的想法。";
let guideHtml = "";
if (wr?.kind === "pick_creation_step" || isFlowPlanReview(view)) {
if (canOfferSaveProduct(view)) {
title = "可以落档了";
hint = view.hasProduct
? "产物已拆出,可开玩;创作流程还在,补节点或再保存一份新定稿。"
: "保存后创作流程仍在;拆出的产物可以单独开玩、存档。";
} else if (wr?.kind === "pick_creation_step" || isFlowPlanReview(view)) {
title = "选要做的节点";
hint = "点可进入的节点即确认并开始;虚线原型点一下增殖再进去。要改排在底栏写意见再发。";
} else if (showOpening) {
@@ -4350,13 +4386,8 @@ function renderSpeakWorkspace(view) {
? proposedOutputCopy(view.proposedNextStep)
: view.focus?.detail || "确认执行,或在底栏说明意见。";
} else if (wr?.kind === "input") {
if (wr.message && /创作已收口/.test(wr.message)) {
title = "创作已收口";
hint = wr.message;
} else {
title = "继续说";
hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。";
}
title = "继续说";
hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。";
} else if (view.uiPrompt && !hasUserMessages(view)) {
title = "描述你想创作什么";
hint = String(view.uiPrompt).trim().slice(0, 280);
@@ -4377,9 +4408,23 @@ function renderSpeakWorkspace(view) {
interactive: flowInteractive,
})
: "";
const enterPlay = canOfferEnterPlay(view)
? `<div class="workspace-intent-actions">
${
canOfferSaveProduct(view)
? `<button type="button" class="btn btn-primary" data-save-product>保存定稿</button>`
: ""
}
<button type="button" class="btn${
canOfferSaveProduct(view) ? "" : " btn-primary"
}" data-enter-play>${view.hasProduct ? "用产物开玩" : "开玩"}</button>
</div>`
: "";
return `<section class="workspace-intent">
<div class="workspace-intent-head">
<span class="workspace-surface-kicker">${flowInteractive ? "流程" : "说话"}</span>
<span class="workspace-surface-kicker">${
canOfferSaveProduct(view) ? "落档" : flowInteractive ? "流程" : "说话"
}</span>
${leaveStepControlHtml(view)}
</div>
<h2 class="workspace-intent-title">${esc(title)}</h2>
@@ -4388,6 +4433,7 @@ function renderSpeakWorkspace(view) {
${guideHtml}
${intake}
${flowHtml}
${enterPlay}
</section>`;
}
@@ -4586,6 +4632,12 @@ export function renderMessageFeed(view, loading, handlers = {}) {
stage.innerHTML = renderSpeakWorkspace(view);
wireCreationFlowGraph(stage, handlers);
wireLeaveStepControl(stage, handlers);
stage.querySelector("[data-enter-play]")?.addEventListener("click", () => {
handlers.onEnterPlay?.();
});
stage.querySelector("[data-save-product]")?.addEventListener("click", () => {
handlers.onSaveProduct?.();
});
if (panelFeed) ensureQuestionsHostIn(panelFeed);
return;
}