引入游玩回合与开场收口,抽出可替换 agent 司机并支持运行中止;完善预设洞位、世界书拼装与试跑,并打磨会话运行时与 Web 交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 01:13:48 +08:00
co-authored by Cursor
parent ec474cb946
commit b82da4c5a5
95 changed files with 6781 additions and 1344 deletions
+514 -73
View File
@@ -12,6 +12,7 @@ import {
parsePresentDoc,
presentFromPlain,
renderPresentShellHtml,
stripPresentSourceFences,
} from "./present-shells.js";
const HIDE_KINDS = new Set(["worker_stub"]);
@@ -57,6 +58,10 @@ function esc(s) {
.replace(/>/g, "&gt;");
}
function escAttr(s) {
return esc(s).replace(/"/g, "&quot;");
}
function renderThinkingBlock(thinking, { open = false } = {}) {
const text = (thinking ?? "").trim();
if (!text) return "";
@@ -66,23 +71,39 @@ function renderThinkingBlock(thinking, { open = false } = {}) {
</details>`;
}
function isPlayView(view) {
return Boolean(view?.playLayerActive || view?.lifecycleStage === "play");
}
function renderLiveStreamBody(live, view) {
const thinking = (live?.thinking || view?.agentThinking || "").trim();
const output = (live?.output ?? "").trim();
const showOutput =
Boolean(output) && (view?.lifecycleStage ?? document.body.dataset.lifecycle) === "play";
const parts = [];
if (thinking) {
parts.push(
`<section class="msg-live-section"><header>思考</header><pre class="msg-live-pre">${esc(thinking)}</pre></section>`,
);
}
if (showOutput) {
if (output) {
parts.push(
`<section class="msg-live-section"><header>输出</header><pre class="msg-live-pre">${esc(output)}</pre></section>`,
);
}
return parts.length ? parts.join("") : "…";
return parts.length
? parts.join("")
: `<p class="msg-live-empty">正在生成,尚未收到内容</p>`;
}
function renderRetryRunButton() {
return `<button type="button" class="msg-action msg-action-primary" data-act="retry_run" title="停止当前生成并重试">↻</button>`;
}
function wireRetryRun(root, handlers) {
root?.querySelector("[data-act=retry_run]")?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
handlers.onRetryRun?.();
});
}
function fmtTime(iso) {
@@ -141,13 +162,19 @@ function hideMsgMenu() {
const menu = document.getElementById("msg-action-menu");
if (menu) menu.hidden = true;
msgMenuState.messageId = null;
msgMenuState.contextMessageId = null;
msgMenuState.rollbackMessageId = null;
msgMenuState.rollbackMode = "keep";
msgMenuState.restoreDraft = "";
msgMenuState.card = null;
}
const msgMenuState = {
messageId: null,
contextMessageId: null,
rollbackMessageId: null,
rollbackMode: "keep",
restoreDraft: "",
card: null,
handlers: null,
};
@@ -155,25 +182,38 @@ const msgMenuState = {
function showMsgMenu(card, x, y) {
const menu = document.getElementById("msg-action-menu");
if (!menu || !card) return;
const isReview = card.dataset.review === "1";
const isReadOnly = card.dataset.readOnly === "1";
const canEdit = !isReview && card.dataset.canEdit === "1";
const canDelete = !isReview && !isReadOnly && Boolean(card.dataset.messageId);
const canRollback = !isReview && !isReadOnly && Boolean(card.dataset.rollbackMessageId);
const hasContext = card.dataset.hasContext === "1";
const pack = card.closest(".coord-pack");
const rollbackHost = pack || card;
// 上下文按单次请求(这条消息)区分,不能回落到整包里的另一条
const contextTarget = card.dataset.hasContext === "1" ? card : null;
const isReview = card.dataset.review === "1" || pack?.dataset.review === "1";
const isReadOnly = card.dataset.readOnly === "1" || pack?.dataset.readOnly === "1";
const canEdit = !isReview && !pack && card.dataset.canEdit === "1";
const canDelete = !isReview && !isReadOnly && !pack && Boolean(card.dataset.messageId);
const rollbackMessageId = rollbackHost.dataset.rollbackMessageId ?? null;
const rollbackMode = rollbackHost.dataset.rollbackMode || "keep";
const canRollback = !isReview && !isReadOnly && Boolean(rollbackMessageId);
const hasContext = Boolean(contextTarget);
const editBtn = document.getElementById("msg-menu-edit");
const delBtn = document.getElementById("msg-menu-delete");
const rollbackBtn = document.getElementById("msg-menu-rollback");
const ctxBtn = document.getElementById("msg-menu-context");
if (editBtn) editBtn.toggleAttribute("hidden", !canEdit);
if (delBtn) delBtn.toggleAttribute("hidden", !canDelete);
if (rollbackBtn) rollbackBtn.toggleAttribute("hidden", !canRollback);
if (rollbackBtn) {
rollbackBtn.toggleAttribute("hidden", !canRollback);
rollbackBtn.textContent =
rollbackMode === "pending-input" ? "回到这一轮输入" : "回退到这里";
}
if (ctxBtn) ctxBtn.toggleAttribute("hidden", !hasContext);
menu.hidden = false;
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
msgMenuState.messageId = card.dataset.messageId ?? null;
msgMenuState.rollbackMessageId = card.dataset.rollbackMessageId ?? null;
msgMenuState.contextMessageId = contextTarget?.dataset.messageId ?? card.dataset.messageId ?? null;
msgMenuState.rollbackMessageId = rollbackMessageId;
msgMenuState.rollbackMode = rollbackMode;
msgMenuState.restoreDraft = rollbackHost.dataset.restoreDraft ?? "";
msgMenuState.card = card;
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth) {
@@ -193,12 +233,15 @@ function wireMsgActionMenu(handlers) {
menu.addEventListener("click", async (e) => {
const action = e.target.closest("[data-msg-menu-action]")?.getAttribute("data-msg-menu-action");
const messageId = msgMenuState.messageId;
const contextMessageId = msgMenuState.contextMessageId || messageId;
const rollbackMessageId = msgMenuState.rollbackMessageId;
const rollbackMode = msgMenuState.rollbackMode;
const restoreDraft = msgMenuState.restoreDraft;
const card = msgMenuState.card;
hideMsgMenu();
if (!messageId || !action) return;
const bodyEl = card?.querySelector(".msg-body:not(.msg-body-editing), .coord-text");
const body = bodyEl?.textContent ?? card?.dataset.originalText ?? "";
const body = card?.dataset.originalText || bodyEl?.textContent || "";
if (action === "copy") {
try {
@@ -209,7 +252,7 @@ function wireMsgActionMenu(handlers) {
return;
}
if (action === "view-context") {
msgMenuState.handlers?.onViewContext?.(messageId);
msgMenuState.handlers?.onViewContext?.(contextMessageId);
return;
}
if (action === "edit") {
@@ -218,8 +261,20 @@ function wireMsgActionMenu(handlers) {
}
if (action === "rollback") {
if (!rollbackMessageId) return;
if (!confirm("回退到这条消息?之后的对话和创作状态将被移除。")) return;
msgMenuState.handlers?.onDeleteMessage?.(rollbackMessageId);
const pendingInput = rollbackMode === "pending-input";
const confirmed = pendingInput
? confirm(
"回到这一轮的待输入?这一轮及之后的对话和创作状态将被移除,原文会填回底栏。",
)
: confirm("回退到这条消息?之后的对话和创作状态将被移除。");
if (!confirmed) return;
if (pendingInput) {
const restore = msgMenuState.handlers?.onRollbackToInput;
if (typeof restore === "function") restore(rollbackMessageId, restoreDraft);
else msgMenuState.handlers?.onDeleteMessage?.(rollbackMessageId);
} else {
msgMenuState.handlers?.onDeleteMessage?.(rollbackMessageId);
}
return;
}
if (action === "delete") {
@@ -234,7 +289,7 @@ function wireMessageContextMenu(feed, handlers) {
feed.dataset.contextWired = "1";
wireMsgActionMenu(handlers);
feed.addEventListener("contextmenu", (e) => {
const bubble = e.target.closest(".msg-bubble, .workspace-review, .coord-line");
const bubble = e.target.closest(".msg-bubble, .workspace-review, .coord-line, .coord-pack");
const card = e.target.closest("[data-message-id]");
if (!bubble || !card || card.classList.contains("msg-pending")) return;
e.preventDefault();
@@ -423,22 +478,40 @@ function formatQuestionsHtml(body) {
}
/** play 期助手终稿(含已验收的用户展示)用呈现壳渲染 */
function isPlayHousekeepingMessage(msg) {
const title = String(msg.title ?? "");
const text = String(msg.text ?? msg.body ?? "").trim();
if (title === "上下文已压缩" || title === "阶段机") return true;
if (/^\[上下文已压缩\]/.test(text)) return true;
if (/^\[裁决合并\]/.test(text)) return true;
if (/^\[旁观维护\]/.test(text)) return true;
if (/归档\s*\d+\s*个过程 tag/.test(text)) return true;
return false;
}
function shouldRenderPlayPresent(msg, view) {
if (view?.lifecycleStage !== "play") return false;
if (!isPlayView(view)) return false;
if (msg.role === "user") return false;
const kind = msg.kind ?? "";
if (kind === "worker_questions" || kind === "orchestrator_thinking") return false;
if (kind === "worker_output") return false; // 已走 formatArtifactBodyHtml
if (isPlayHousekeepingMessage(msg)) return false;
const actor = String(msg.actor ?? "");
if (actor === "narrator" || /用户展示|开场白/.test(String(msg.title ?? ""))) {
return true;
}
// 助手可见终稿:无特殊 kind 的长文
return kind === "system_info" || kind === "worker_stub" || !kind;
const title = String(msg.title ?? "");
return actor === "narrator" || actor === "round-present" || /用户展示|开场白/.test(title);
}
function isPlayFinalReply(msg, view) {
if (!isPlayView(view) || msg.role === "user") return false;
return shouldRenderPlayPresent(msg, view);
}
function isPlayProcessMessage(msg, view) {
if (!isPlayView(view) || msg.role === "user") return false;
return !isPlayFinalReply(msg, view);
}
function formatPlayPresentHtml(body, view) {
const trimmed = (body || "").trim();
const trimmed = stripPresentSourceFences((body || "").trim());
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
const tweaks = view?.presentationTweaks;
const fallbackShell =
@@ -562,7 +635,7 @@ function wireRailChrome() {
function hasStartedCreation(view) {
if (!view?.id) return false;
if (view.lifecycleStage === "play") return true;
if (isPlayView(view)) return true;
if (hasUserMessages(view)) return true;
const reason = view.waitingReason?.kind;
if (reason && reason !== "intake") return true;
@@ -626,6 +699,7 @@ function shouldShowInFeed(msg, view) {
if (HIDE_KINDS.has(kind)) return false;
if (FEED_HIDDEN_KINDS.has(kind)) return false;
if (msg.compressed) return false;
if (isPlayView(view) && isPlayHousekeepingMessage(msg)) return false;
if (isSkillSelectionMessage(msg)) return false;
if (isReviewSidecarQuestionStub(msg)) return false;
if (
@@ -635,11 +709,11 @@ function shouldShowInFeed(msg, view) {
) {
return false;
}
if (kind === "worker_output" && view.waitingReason?.kind === "review_artifact") {
if (kind === "worker_output" && view.waitingReason?.kind === "review_artifact" && !isPlayView(view)) {
return false;
}
// 创作:编排器思考进「调度」,不占主区;游玩保持原样
if (kind === "orchestrator_thinking" && view.lifecycleStage !== "play") {
// 创作:编排器思考进「调度」,不占主区;游玩保持原样(默认折叠)
if (kind === "orchestrator_thinking" && !isPlayView(view)) {
return false;
}
if (kind === "orchestrator_prompt") return false;
@@ -661,10 +735,10 @@ function maybeSyncRail(view) {
}
export function renderLifecycle(view) {
const toggle = document.getElementById("lifecycle-toggle");
if (!toggle) return;
const stage = view.lifecycleStage ?? "design";
document.body.dataset.lifecycle = stage;
const toggle = document.getElementById("lifecycle-toggle");
if (!toggle) return;
toggle.querySelectorAll("[data-stage]").forEach((btn) => {
const s = btn.getAttribute("data-stage");
btn.classList.toggle("active", s === stage);
@@ -1541,7 +1615,7 @@ function renderKnownArtifactHtml(doc, opts = {}) {
if (schema === "context-fragment.v1" || isContextFragmentLike(doc)) {
return renderContextFragmentHtml(doc, opts);
}
if (schema === "context-order.v1" || Array.isArray(doc.slots)) {
if (schema === "context-order.v1" || Array.isArray(doc.slots) || Array.isArray(doc.agents)) {
const orderHtml = renderContextOrderHtml(doc);
if (orderHtml) return orderHtml;
}
@@ -1666,6 +1740,105 @@ function renderContextFragmentHtml(doc, opts = {}) {
return `<div class="artifact-friendly artifact-context-fragment">${parts.join("")}</div>`;
}
/** 开场白终节点:1~多条候选用呈现壳展示,选定后由程序落库 */
function collectOpeningCandidateTexts(body) {
const texts = [];
const push = (value) => {
const text = String(value ?? "").trim();
if (text && !texts.includes(text)) texts.push(text);
};
push(body.开场白全文);
if (Array.isArray(body.开场白候选)) {
for (const item of body.开场白候选) {
if (typeof item === "string") push(item);
else if (item && typeof item === "object") {
push(item.全文 || item.text || item.开场白 || item.内容);
}
}
}
return texts;
}
function renderOpeningSetupBodyHtml(body) {
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
const candidates = collectOpeningCandidateTexts(body);
if (!candidates.length) return "";
const total = candidates.length;
const slides = candidates
.map((text, i) => {
const hidden = i === 0 ? "" : " hidden";
const selected = i === 0 ? " is-selected" : "";
return `<article class="opening-candidate${selected}" data-opening-index="${i}"${hidden}>${renderPresentShellHtml(
presentFromPlain(text, "prose"),
esc,
)}</article>`;
})
.join("");
const nav =
total > 1
? `<div class="opening-picker-nav">
<button type="button" class="opening-picker-btn" data-opening-delta="-1">上一版</button>
<span class="opening-picker-pos" data-opening-pos>1 / ${total}</span>
<button type="button" class="opening-picker-btn" data-opening-delta="1">下一版</button>
</div>`
: "";
const vars = Array.isArray(body.开场变量) ? body.开场变量 : [];
const varsHtml = vars.length
? `<section class="artifact-block"><h4>开场变量</h4><ul class="artifact-list">${vars
.map((row) => {
const name = row?.名 || row?.name || row?.key || "";
const val = row?.值 !== undefined ? row.值 : row?.value;
const note = row?.依据 || row?.note || "";
const noteHtml = note
? ` <span class="muted">(${esc(String(note))})</span>`
: "";
return `<li><strong>${esc(String(name))}</strong>:${esc(String(val ?? ""))}${noteHtml}</li>`;
})
.join("")}</ul></section>`
: "";
const space = body.用户可行动空间
? `<section class="artifact-block"><h4>可行动空间</h4>${renderProseHtml(
body.用户可行动空间,
)}</section>`
: "";
return `<div class="opening-setup-body">
<div class="opening-picker" data-opening-picker="1" data-selected="0" data-total="${total}">
${slides}
${nav}
</div>
${varsHtml}
${space}
</div>`;
}
function wireOpeningPicker() {
document.querySelectorAll("[data-opening-picker]").forEach((root) => {
if (root.dataset.wired === "1") return;
root.dataset.wired = "1";
const total = Math.max(1, Number(root.getAttribute("data-total")) || 1);
const show = (index) => {
const next = ((index % total) + total) % total;
root.setAttribute("data-selected", String(next));
root.querySelectorAll("[data-opening-index]").forEach((el) => {
const i = Number(el.getAttribute("data-opening-index"));
const on = i === next;
el.toggleAttribute("hidden", !on);
el.classList.toggle("is-selected", on);
});
const pos = root.querySelector("[data-opening-pos]");
if (pos) pos.textContent = `${next + 1} / ${total}`;
};
root.addEventListener("click", (e) => {
const btn = e.target.closest("[data-opening-delta]");
if (!btn || !root.contains(btn)) return;
const delta = Number(btn.getAttribute("data-opening-delta"));
if (delta !== 1 && delta !== -1) return;
const cur = Number(root.getAttribute("data-selected") || 0);
show(cur + delta);
});
});
}
/** 按技能分流正文视图;无专用模板则返回 ""(调用方走通用结构化) */
function renderSpecialtyBodyHtml(body, skill) {
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
@@ -1703,6 +1876,22 @@ function renderSpecialtyBodyHtml(body, skill) {
const html = renderNarrativeBodyHtml(body);
if (html) return html;
}
if (
body.呈现壳 != null ||
body.可见块 != null ||
(body.示例 != null && (skillName.includes("正文组成") || skillName.includes("回复格式")))
) {
const html = renderReplyFormatBodyHtml(body);
if (html) return html;
}
if (
body.开场白全文 != null ||
body.开场白候选 != null ||
skillName.includes("开场白")
) {
const html = renderOpeningSetupBodyHtml(body);
if (html) return html;
}
const looksAesthetics =
body.设定逻辑 != null ||
@@ -1782,6 +1971,101 @@ function renderSpecialtyBodyHtml(body, skill) {
</div>`;
}
/** 正文组成 · 壳适配 + 示例灌数(present 壳预览,验收美化) */
function renderReplyFormatBodyHtml(body) {
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
const shellDoc = body.呈现壳 && typeof body.呈现壳 === "object" ? body.呈现壳 : {};
const shellId =
typeof shellDoc.shell_id === "string" && PRESENT_SHELL_IDS.includes(shellDoc.shell_id)
? shellDoc.shell_id
: typeof shellDoc.shell_id === "string"
? shellDoc.shell_id
: "";
const tweaks =
shellDoc.微调 && typeof shellDoc.微调 === "object" && !Array.isArray(shellDoc.微调)
? shellDoc.微调
: {};
const example = body.示例 && typeof body.示例 === "object" ? body.示例 : null;
const packetRaw = example?.灌数;
const presentView =
packetRaw && typeof packetRaw === "object"
? parsePresentDoc(
{
...packetRaw,
shell: packetRaw.shell || packetRaw.shell_id || shellId || "prose",
},
shellId || "prose",
)
: null;
const parts = [];
parts.push(`<div class="af-split-banner" role="note">
<span><b>本步看壳与美化</b></span>
<span>示例非正史 · 文笔不重要</span>
</div>`);
const metaBits = [];
if (shellId) {
metaBits.push(`<span class="ws-badge ws-badge-continue">壳 ${esc(shellId)}</span>`);
}
if (shellDoc.为何选它) {
metaBits.push(`<span class="ws-muted">${esc(String(shellDoc.为何选它))}</span>`);
}
if (body.版式隐喻) {
metaBits.push(`<span class="ws-muted">隐喻:${esc(String(body.版式隐喻))}</span>`);
}
if (metaBits.length) {
parts.push(`<div class="artifact-chip-row reply-format-shell-meta">${metaBits.join("")}</div>`);
}
if (presentView) {
const nature = example?.性质 ? esc(String(example.性质)) : "验收预览|非正史";
const explain = example?.用来说明 ? `<p class="ws-muted">${esc(String(example.用来说明))}</p>` : "";
parts.push(`<section class="af-panel af-panel-wide reply-format-preview">
<h4>版式预览 <span class="ws-badge">${nature}</span></h4>
${explain}
${renderPresentShellHtml(presentView.packet, esc, { tweaks })}
</section>`);
} else {
parts.push(
`<section class="af-panel"><p class="ws-muted">(暂无示例灌数;定稿时应给 present.v1 预览)</p></section>`,
);
}
const contractKeys = [
"依据的体验与呈现",
"呈现壳",
"版式隐喻",
"可见块",
"监控栏锚点",
"隐藏段",
"前端拆分与美化",
"拼装与终稿",
];
const used = new Set(["示例"]);
for (const key of contractKeys) {
if (body[key] == null || body[key] === "") continue;
used.add(key);
parts.push(
`<details class="skill-example reply-format-contract"><summary>${esc(key)}</summary>${renderStructuredValueHtml(
body[key],
0,
)}</details>`,
);
}
for (const [k, v] of Object.entries(body)) {
if (used.has(k) || v == null || v === "") continue;
parts.push(
`<details class="skill-example reply-format-contract"><summary>${esc(k)}</summary>${renderStructuredValueHtml(
v,
0,
)}</details>`,
);
}
return `<div class="artifact-friendly artifact-reply-format">${parts.join("")}</div>`;
}
/** 叙事指南 · 一份全文结构化卡(不强调双投影裁剪) */
function renderNarrativeBodyHtml(body) {
const order = [
@@ -2528,7 +2812,10 @@ function renderSelfScoreHtml(自评) {
function renderProbeHtml(追问) {
if (!追问 || typeof 追问 !== "object") return "";
const qs = Array.isArray(追问.题目) ? 追问.题目 : [];
const qs = Array.isArray(追问.题目)
? 追问.题目.filter((q) => q && typeof q === "object" && (q.问 || q.prompt))
: [];
if (!qs.length) return "";
const cards = [];
if (追问.导语) {
cards.push(`<p class="artifact-probe-lead">${esc(String(追问.导语))}</p>`);
@@ -2555,7 +2842,13 @@ function renderProbeHtml(追问) {
}
function renderContextOrderHtml(doc) {
if (!Array.isArray(doc.slots) || !doc.slots.length) return null;
const slots =
Array.isArray(doc.slots) && doc.slots.length
? doc.slots
: Array.isArray(doc.agents)
? doc.agents.filter((a) => a && a.enabled !== false)
: [];
if (!slots.length) return null;
const parts = [];
if (doc.brief) {
parts.push(
@@ -2564,7 +2857,7 @@ function renderContextOrderHtml(doc) {
}
for (const slot of doc.slots) {
if (!slot || typeof slot !== "object") continue;
const title = slot.label || slot.ref || "槽";
const title = slot.label || slot.ref || slot.id || "槽";
const inserts = Array.isArray(slot.inserts) ? slot.inserts : [];
if (!inserts.length && Array.isArray(slot.order)) {
// 扁平序:直接是条目列表
@@ -2919,7 +3212,7 @@ function renderCreationFlowView(flowView) {
return `<div class="review-parse-error" role="alert">
<div class="review-parse-error-title">流程无法解析</div>
<p>${esc(flowView.parseError)}</p>
<p class="review-parse-error-hint">需要 JSON:steps 数组,每步含 name(与可选 id)、depends_on、可选 params;可含 status=open|closed。</p>
<p class="review-parse-error-hint">需要 JSON:steps 数组,每步含 name(与可选 id)、depends_on、可选 params;可含 status=open|closed。回头修改用 mode=revise 与 revises。</p>
</div>`;
}
if (!flowView.steps?.length) return "";
@@ -2943,7 +3236,9 @@ function renderCreationFlowView(flowView) {
? s.depends_on.map((d) => esc(d)).join("、")
: "无";
const occ =
s.occurrence && s.occurrence > 1
s.mode === "revise"
? `<span class="flow-occ flow-occ-revise">回头修改</span>`
: s.occurrence && s.occurrence > 1
? `<span class="flow-occ">第 ${esc(String(s.occurrence))} 次</span>`
: s.repeatable
? `<span class="flow-occ">可反复</span>`
@@ -3068,12 +3363,16 @@ function renderReviewWorkspace(review, opts = {}) {
asPresentFallback:
review.workerId === "narrator" ||
review.workerId === "opening-generator" ||
(Array.isArray(review.outputTags) &&
review.outputTags.includes("设计.开场白与开场变量")) ||
/用户展示|开场白/.test(review.body || ""),
});
const contextId = review.sourceMessageId || review.id || "";
const hasContext = review.contextTrace ? "1" : "0";
const label = displayWorkerLabel(review.workerId) || "产物";
const copy = reviewComposerCopy(review.workerId);
const copy = reviewComposerCopy(review.workerId, {
outputTags: review.outputTags,
});
const sourceMsg =
opts.sourceMessage ||
(review.sourceMessageId
@@ -3211,25 +3510,65 @@ export function renderAgentPanel(view, loading) {
timelineEl.scrollTop = timelineEl.scrollHeight;
}
function coordOriginalText(msg) {
return String(msg?.body ?? msg?.text ?? "").trim();
}
/** 一轮 = 一次用户输入 + 直到下一次输入前的全部输出。 */
function groupCoordTurnPacks(messages) {
const packs = [];
for (const msg of messages) {
const isUser = msg.role === "user";
const last = packs[packs.length - 1];
if (isUser || !last) {
packs.push({ user: isUser ? msg : null, outputs: isUser ? [] : [msg] });
} else {
last.outputs.push(msg);
}
}
return packs;
}
function packAnchorMessage(pack) {
return pack.user || pack.outputs[0] || null;
}
function packLastMessage(pack) {
return pack.outputs[pack.outputs.length - 1] || pack.user || null;
}
function renderCoordLine(msg, view, opts = {}) {
const isUser = msg.role === "user";
const kind = msg.kind ?? (isUser ? "user_input" : "system_info");
const who = isUser ? "你" : msgLabel(msg);
const full = coordOriginalText(msg) || msgBody(msg, view);
const raw = msgBody(msg, view).replace(/\s+/g, " ").trim();
const text = raw.length > 320 ? `${raw.slice(0, 320)}…` : raw;
const nav = renderVariantNavHtml(msg);
const inPack = opts.inPack === true;
const allMessages = view.messages ?? [];
const messageIndex = allMessages.findIndex((item) => item.id === msg.id);
const rollbackMessageId = messageIndex >= 0 ? allMessages[messageIndex + 1]?.id : null;
const rollbackMessageId = inPack ? null : getRollbackMessageId(allMessages, msg.id);
const rollbackAttr = rollbackMessageId
? ` data-rollback-message-id="${esc(rollbackMessageId)}"`
: "";
const contextAttr = msg.contextTrace ? ` data-has-context="1"` : "";
const readOnlyAttr = opts.traceOnly ? ` data-read-only="1"` : "";
const originalAttr = full ? ` data-original-text="${escAttr(full)}"` : "";
const menu = inPack
? ""
: `<button type="button" class="coord-line-menu" data-msg-menu-trigger aria-label="打开这条消息的操作菜单" title="更多操作">⋯</button>`;
const trace = msg.contextTrace;
const variantTotal = Math.max(1, Number(msg.branchTotal) || 1);
const variantIndex = Math.min(variantTotal, Math.max(1, (msg.branchIndex ?? 0) + 1));
const requestBits = [
opts.requestIndex ? `请求 ${opts.requestIndex}` : "",
variantTotal > 1 ? `${variantIndex}/${variantTotal} 版` : "",
].filter(Boolean);
const requestHint = requestBits.length ? requestBits.join(" · ") : "本次请求";
const traceMeta = trace
? `<button type="button" class="coord-context-trigger" data-context-trigger aria-label="查看 ${esc(who)} 的完整请求上下文">
? `<button type="button" class="coord-context-trigger" data-context-trigger aria-label="查看 ${esc(who)} 的${esc(requestHint)}上下文">
<span class="coord-context-label">保留上下文</span>
<span class="coord-context-request">${esc(requestHint)}</span>
<span>${trace.messages?.length ?? 0} 段</span>
<span>${((trace.charCount ?? 0) / 1024).toFixed(1)} KB</span>
</button>
@@ -3240,14 +3579,73 @@ function renderCoordLine(msg, view, opts = {}) {
</div>`
: "";
const traceClass = trace ? " coord-line--trace" : "";
return `<div class="coord-line ${isUser ? "coord-user" : "coord-agent"}${traceClass}" data-message-id="${esc(msg.id)}" data-kind="${esc(kind)}"${rollbackAttr}${contextAttr}${readOnlyAttr}>
<button type="button" class="coord-line-menu" data-msg-menu-trigger aria-label="打开这条消息的操作菜单" title="更多操作">⋯</button>
return `<div class="coord-line ${isUser ? "coord-user" : "coord-agent"}${traceClass}" data-message-id="${esc(msg.id)}" data-kind="${esc(kind)}"${rollbackAttr}${contextAttr}${readOnlyAttr}${originalAttr}>
${menu}
<div class="coord-who"><span>${esc(who)}</span><span class="coord-kind">${esc(kind)}</span><span class="coord-time">${fmtTime(msg.createdAt)}</span>${nav}</div>
<div class="coord-text">${esc(text || "(空)")}</div>
${traceMeta}
</div>`;
}
function renderCoordPack(pack, view, opts = {}) {
const anchor = packAnchorMessage(pack);
if (!anchor) return "";
const last = packLastMessage(pack);
const pending = Boolean(pack.user) && pack.outputs.length === 0;
const userText = pack.user ? coordOriginalText(pack.user) : "";
const allMessages = view.messages ?? [];
let rollbackMode = "";
let rollbackId = "";
if (pack.user) {
rollbackMode = "pending-input";
rollbackId = pack.user.id;
} else if (last) {
const nextId = getRollbackMessageId(allMessages, last.id);
if (nextId) {
rollbackMode = "keep";
rollbackId = nextId;
}
}
const inCount = pack.user ? 1 : 0;
const outCount = pack.outputs.length;
const meta = pending ? "待输入" : `${inCount} 入 · ${outCount} 出`;
const rollbackAttr = rollbackId
? ` data-rollback-message-id="${esc(rollbackId)}" data-rollback-mode="${esc(rollbackMode)}"`
: "";
const restoreAttr =
rollbackMode === "pending-input" && userText
? ` data-restore-draft="${escAttr(userText)}"`
: "";
const originalAttr = userText ? ` data-original-text="${escAttr(userText)}"` : "";
const readOnlyAttr = opts.traceOnly ? ` data-read-only="1"` : "";
const tracedOutputCount = pack.outputs.filter((msg) => msg.contextTrace).length;
let requestIndex = 0;
const lines = [
pack.user
? renderCoordLine(pack.user, view, {
inPack: true,
traceOnly: opts.traceOnly || opts.hiddenIds?.has(pack.user.id),
})
: "",
...pack.outputs.map((msg) => {
const thisRequest =
tracedOutputCount > 1 && msg.contextTrace ? ++requestIndex : 0;
return renderCoordLine(msg, view, {
inPack: true,
traceOnly: opts.traceOnly || opts.hiddenIds?.has(msg.id),
requestIndex: thisRequest || undefined,
});
}),
].join("");
return `<article class="coord-pack${pending ? " coord-pack--pending" : ""}" data-message-id="${esc(anchor.id)}"${rollbackAttr}${restoreAttr}${originalAttr}${readOnlyAttr}>
<button type="button" class="coord-line-menu" data-msg-menu-trigger aria-label="打开这一轮的操作菜单" title="更多操作">⋯</button>
<header class="coord-pack-head">
<span class="coord-pack-meta">${esc(meta)}</span>
</header>
${lines}
</article>`;
}
function getRollbackMessageId(messages, messageId) {
const messageIndex = messages.findIndex((item) => item.id === messageId);
return messageIndex >= 0 ? messages[messageIndex + 1]?.id ?? null : null;
@@ -3255,7 +3653,7 @@ function getRollbackMessageId(messages, messageId) {
/** @returns {"speak"|"answer"|"review"|"busy"|null} */
function resolveDesignSurface(view, loading) {
if (view.lifecycleStage === "play") return null;
if (isPlayView(view)) return null;
if (view.phase === "done") return null;
const busy = Boolean(loading || (view.phase === "running" && !view.waitingReason));
if (busy) return "busy";
@@ -3300,6 +3698,12 @@ function proposedOutputCopy(proposed) {
typeof target === "string" || typeof target === "number"
? String(target).trim()
: "";
if (proposed?.mode === "revise") {
if (targetText) {
return `将在既有「${targetText}」的${proposed.name}上继续改,不是从零再生成。`;
}
return `将在既有「${proposed?.name || "该步"}」上继续改,不是从零再生成。`;
}
if (targetText) {
return `将生成「${targetText}」的${proposed.name}。`;
}
@@ -3347,17 +3751,24 @@ function renderSpeakWorkspace(view) {
hint = "写清楚要改哪里;发送后在现有产物上修改。";
} else if (wr?.kind === "next_intent") {
title = "下一步想写什么";
hint = "说说接下来想做什么;可留空,发送后会展示下一节点供确认。";
hint = "说说接下来想做什么;可留空,也可说回头改某步。发送后会展示下一节点供确认。";
} else if (wr?.kind === "approve_step") {
title = view.proposedNextStep?.name
? `接下来生成 · ${view.proposedNextStep.name}`
? view.proposedNextStep.mode === "revise"
? `回头修改 · ${view.proposedNextStep.name}`
: `接下来生成 · ${view.proposedNextStep.name}`
: "确认下一步";
hint = view.proposedNextStep
? proposedOutputCopy(view.proposedNextStep)
: view.focus?.detail || "确认执行,或在底栏说明意见。";
} else if (wr?.kind === "input") {
title = "继续说";
hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。";
if (wr.message && /创作已收口/.test(wr.message)) {
title = "创作已收口";
hint = wr.message;
} else {
title = "继续说";
hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。";
}
} else if (view.uiPrompt && !hasUserMessages(view)) {
title = "描述你想创作什么";
hint = String(view.uiPrompt).trim().slice(0, 280);
@@ -3400,19 +3811,25 @@ function fillCoordRail(visible, view, handlers) {
return visibleIds.has(message.id) || Boolean(message.contextTrace);
});
const traceCount = history.filter((message) => message.contextTrace).length;
const packs = groupCoordTurnPacks(history);
const hiddenIds = new Set(
history.filter((message) => !visibleIds.has(message.id)).map((message) => message.id),
);
coordRail.hidden = false;
if (drawerBody) {
drawerBody.innerHTML = history.length
? history
.map((m) => renderCoordLine(m, view, { traceOnly: !visibleIds.has(m.id) }))
.join("")
drawerBody.innerHTML = packs.length
? packs.map((pack) => renderCoordPack(pack, view, { hiddenIds })).join("")
: `<p class="coord-empty">暂无对话摘要</p>`;
wireMessageFeedActions(drawerBody, handlers);
wireMessageContextMenu(drawerBody, handlers);
}
if (drawerCount) {
drawerCount.textContent = history.length ? String(history.length) : "";
drawerCount.title = traceCount ? `${traceCount} 条保留了完整请求上下文` : "";
drawerCount.textContent = packs.length ? String(packs.length) : "";
drawerCount.title = traceCount
? `${packs.length} 轮,其中 ${traceCount} 条保留了完整请求上下文`
: packs.length
? `${packs.length} 轮对话`
: "";
}
syncCoordRailChrome();
}
@@ -3444,6 +3861,7 @@ function mountReviewWorkspace(stage, view, handlers, opts = {}) {
}) + askSlot;
wireMessageFeedActions(stage, handlers);
wireMessageContextMenu(stage, handlers);
wireOpeningPicker();
if (opts.mountAskCard) {
const askHost = document.getElementById("workspace-review-ask");
if (askHost) ensureQuestionsHostIn(askHost);
@@ -3466,13 +3884,16 @@ export function renderMessageFeed(view, loading, handlers = {}) {
const surface = resolveDesignSurface(view, loading);
const designWorkspace = surface != null;
const reviewingNow =
view.waitingReason?.kind === "review_artifact" && Boolean(view.reviewArtifact);
!isPlayView(view) &&
view.waitingReason?.kind === "review_artifact" &&
Boolean(view.reviewArtifact);
document.body.classList.toggle("is-reviewing", reviewingNow);
document.body.classList.toggle("design-workspace", designWorkspace);
if (reviewingNow) {
document.body.dataset.reviewKind = reviewComposerCopy(
view.reviewArtifact?.workerId,
{ outputTags: view.reviewArtifact?.outputTags },
).kind;
} else {
delete document.body.dataset.reviewKind;
@@ -3529,9 +3950,11 @@ export function renderMessageFeed(view, loading, handlers = {}) {
<header class="workspace-pending-head">
<span class="msg-tag">${esc(label)}</span>
<span class="msg-live-indicator">流式输出中</span>
${renderRetryRunButton()}
</header>
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
</div>`;
wireRetryRun(stage, handlers);
return;
}
@@ -3565,8 +3988,8 @@ export function renderMessageFeed(view, loading, handlers = {}) {
if (!visible.length && !loading) {
const p = document.createElement("p");
p.className = "empty";
if (view.lifecycleStage === "play") {
p.textContent = "游玩模式:Agent 将按 Worker 集调度,推进世界与叙事。";
if (isPlayView(view)) {
p.textContent = "开场已就绪。在下方说你要做什么。";
} else if (view.uiPrompt) {
const recipeLine = view.selectedRecipe?.name
? `\n\n已选配方:${view.selectedRecipe.name}`
@@ -3621,31 +4044,48 @@ export function renderMessageFeed(view, loading, handlers = {}) {
const questionsActive = kind === "worker_questions" && Boolean(activeQuestions);
const showThinking =
!isUser && msg.thinking && view.lifecycleStage === "play";
const bodyHtml = questionsActive
!isUser && msg.thinking && isPlayView(view);
const innerBody = questionsActive
? `<p class="msg-q-index">${esc(body)}</p>`
: kind === "worker_questions"
? formatQuestionsHtml(body)
: kind === "worker_output"
? formatArtifactBodyHtml(body)
: shouldRenderPlayPresent(msg, view)
? formatPlayPresentHtml(body, view)
: shouldRenderPlayPresent(msg, view)
? formatPlayPresentHtml(body, view)
: kind === "worker_output"
? formatArtifactBodyHtml(body)
: esc(body);
card.innerHTML = `
<div class="msg-bubble${questionsActive ? " msg-bubble-index" : ""}">
${isUser ? "" : `<header class="msg-head">${headInner}${menuTrigger}</header>`}
${showThinking ? renderThinkingBlock(msg.thinking) : ""}
<div class="msg-body">${bodyHtml}</div>
${isUser ? `<footer class="msg-foot">${headInner}${menuTrigger}</footer>` : ""}
</div>`;
const process = !isUser && isPlayProcessMessage(msg, view);
const playFinal = !isUser && isPlayFinalReply(msg, view);
if (process) {
card.classList.add("msg-play-process-row");
card.innerHTML = `
<details class="msg-play-process">
<summary>
<span class="msg-tag">${esc(label)}</span>
${showSubtitle ? `<span class="msg-subtitle">${esc(subtitle)}</span>` : ""}
<span class="msg-time">${fmtTime(msg.createdAt)}</span>
</summary>
${showThinking ? renderThinkingBlock(msg.thinking) : ""}
<div class="msg-body">${innerBody}</div>
</details>`;
} else {
if (playFinal) card.classList.add("msg-play-final");
card.innerHTML = `
<div class="msg-bubble${questionsActive ? " msg-bubble-index" : ""}">
${isUser ? "" : `<header class="msg-head">${headInner}${menuTrigger}</header>`}
${showThinking ? renderThinkingBlock(msg.thinking) : ""}
<div class="msg-body">${innerBody}</div>
${isUser ? `<footer class="msg-foot">${headInner}${menuTrigger}</footer>` : ""}
</div>`;
}
feed.appendChild(card);
}
wireMessageFeedActions(feed, handlers);
wireMessageContextMenu(feed, handlers);
if (reviewingNow && view.reviewArtifact && !loading && stage) {
if (reviewingNow && view.reviewArtifact && !loading && stage && !isPlayView(view)) {
const hasHungAsk = Boolean(activeQuestions?.questions?.length);
mountReviewWorkspace(stage, view, handlers, {
hideAskSidecar: hasHungAsk,
@@ -3661,10 +4101,11 @@ export function renderMessageFeed(view, loading, handlers = {}) {
const label = live?.label ?? view.focus?.action ?? "处理中";
pending.innerHTML = `
<div class="msg-bubble">
<header class="msg-head"><span class="msg-tag">${esc(label)}</span><span class="msg-live-indicator">流式输出中</span></header>
<header class="msg-head"><span class="msg-tag">${esc(label)}</span><span class="msg-live-indicator">流式输出中</span>${renderRetryRunButton()}</header>
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
</div>`;
feed.appendChild(pending);
wireRetryRun(pending, handlers);
}
feed.scrollTop = feed.scrollHeight;
@@ -3682,7 +4123,7 @@ export function updateLiveStreamPanel(view) {
if (labelEl && live?.label) labelEl.textContent = live.label;
const next = renderLiveStreamBody(live, view);
const hadThinking = Boolean(body.querySelector(".msg-live-pre"));
if (next === "…" && hadThinking) return;
if (next.includes("msg-live-empty") && hadThinking) return;
body.innerHTML = next;
const pre = body.querySelector(".msg-live-pre:last-of-type");
if (pre) pre.scrollTop = pre.scrollHeight;
+464 -275
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -101,6 +101,10 @@ export function formatWorkerDisplayTitle(workerId, action = null) {
export function reviewComposerCopy(workerId, opts = {}) {
const hasQuestions = Boolean(opts.hasQuestions);
const id = String(workerId ?? "").trim();
const openingReview =
id === "opening-generator" ||
(Array.isArray(opts.outputTags) &&
opts.outputTags.includes("设计.开场白与开场变量"));
if (id === "design-flow") {
return {
@@ -120,7 +124,7 @@ export function reviewComposerCopy(workerId, opts = {}) {
};
}
if (id === "opening-generator") {
if (id === "opening-generator" || openingReview) {
return {
chip: "开场",
kicker: "待选定",
+22 -32
View File
@@ -35,7 +35,6 @@
<div class="rail-panels">
<div class="rail-tab-panel active" id="rail-panel-books" role="tabpanel" data-rail-panel="books">
<nav class="path-bar" id="book-path-bar" aria-label="面包屑" hidden></nav>
<div class="book-list" id="book-list"></div>
<div id="book-box-overlay" class="book-box-overlay" hidden aria-hidden="true"></div>
</div>
@@ -48,39 +47,11 @@
</aside>
<main class="panel panel-main">
<header class="main-head" id="main-head">
<div class="main-head-left">
<h1 class="work-title" id="work-title">未打开作品</h1>
<span class="work-meta" id="work-meta"></span>
</div>
<div class="main-head-center">
<div class="lifecycle-toggle" id="lifecycle-toggle" role="group" aria-label="创作 / 游玩">
<button type="button" class="lifecycle-btn active" data-stage="design">创作</button>
<button type="button" class="lifecycle-btn" data-stage="play" disabled>游玩</button>
</div>
</div>
<div class="main-head-right">
<button type="button" class="btn-sm" id="btn-save-instance" hidden>保存定稿</button>
<button type="button" class="btn-sm" id="btn-save-play" hidden>保存游玩</button>
<button type="button" class="btn-sm" id="btn-export" disabled>导出</button>
<span class="status" id="status-pill"><span class="status-dot" id="status-dot"></span><span id="status-text">—</span></span>
<details class="more-menu">
<summary class="btn-sm more-menu-sum" title="更多页面">更多</summary>
<div class="more-menu-panel" role="menu">
<a class="more-menu-item" href="/modules.html" role="menuitem">技能</a>
<a class="more-menu-item" href="/shells.html" role="menuitem">呈现壳</a>
<a class="more-menu-item" href="/stats.html" role="menuitem">Token</a>
<a class="more-menu-item" href="/settings.html" role="menuitem">设置</a>
</div>
</details>
</div>
</header>
<div class="main-body">
<section class="panel-feed" id="panel-feed">
<div id="skill-picker" class="skill-picker" hidden></div>
<div id="message-feed" class="message-feed">
<p class="empty">点击左侧 + 新建作品</p>
<p class="empty">正在打开作品…</p>
</div>
<div id="workspace-stage" class="workspace-stage" hidden aria-label="验收工作面"></div>
<div id="questions-card-host" class="questions-card-host" hidden></div>
@@ -106,7 +77,22 @@
</aside>
</div>
<footer class="composer" id="composer"></footer>
<footer class="composer" id="composer">
<span class="status" id="status-pill"><span class="status-dot" id="status-dot"></span><span id="status-text">—</span></span>
<div id="composer-main"></div>
<details class="more-menu">
<summary class="btn-sm more-menu-sum" title="更多">更多</summary>
<div class="more-menu-panel" role="menu">
<button type="button" class="more-menu-item" id="btn-save-instance" hidden role="menuitem">保存定稿</button>
<button type="button" class="more-menu-item" id="btn-save-play" hidden role="menuitem">保存游玩</button>
<button type="button" class="more-menu-item" id="btn-export" disabled role="menuitem">导出</button>
<a class="more-menu-item" href="/modules.html" role="menuitem">技能</a>
<a class="more-menu-item" href="/shells.html" role="menuitem">呈现壳</a>
<a class="more-menu-item" href="/stats.html" role="menuitem">Token</a>
<a class="more-menu-item" href="/settings.html" role="menuitem">设置</a>
</div>
</details>
</footer>
</main>
</div>
@@ -133,7 +119,11 @@
</dialog>
<menu id="book-action-menu" class="context-menu" hidden>
<button type="button" class="context-menu-item" id="book-menu-open" data-action="open">打开</button>
<button type="button" class="context-menu-item" id="book-menu-open" data-action="open">打开创作</button>
<button type="button" class="context-menu-item" id="book-menu-play" data-action="play">进入游玩</button>
<button type="button" class="context-menu-item" id="book-menu-save-instance" data-action="save-instance">保存定稿</button>
<button type="button" class="context-menu-item" id="book-menu-save-play" data-action="save-play">保存游玩</button>
<button type="button" class="context-menu-item" id="book-menu-export" data-action="export">导出</button>
<button type="button" class="context-menu-item" id="book-menu-rename" data-action="rename">重命名</button>
<button type="button" class="context-menu-item" id="book-menu-duplicate" data-action="duplicate">复制备份</button>
<button type="button" class="context-menu-item" id="book-menu-select" data-action="select-mode">多选</button>
+16 -1
View File
@@ -82,6 +82,20 @@ function formatBlockContent(value) {
return String(value);
}
/** 模型常把 tag 名写进正文开头;展示前剥掉 */
export function stripPresentSourceFences(raw) {
let s = String(raw ?? "").trim();
for (let i = 0; i < 3; i += 1) {
const next = s
.replace(/^(#{1,6}\s*)?输出[.::]?\s*(用户展示|开场白)(?:\s*\n+|\s*$)/u, "")
.replace(/^【\s*输出[.::]?\s*(用户展示|开场白)\s*】(?:\s*\n+|\s*$)/u, "")
.trim();
if (next === s) break;
s = next;
}
return s;
}
function normalizeBlocks(raw) {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
const out = {};
@@ -90,6 +104,7 @@ function normalizeBlocks(raw) {
}
if (out.body === undefined && raw.正文 !== undefined) out.body = raw.正文;
if (out.monitor === undefined && raw.监控 !== undefined) out.monitor = raw.监控;
if (typeof out.body === "string") out.body = stripPresentSourceFences(out.body);
return out;
}
@@ -162,7 +177,7 @@ export function presentFromPlain(text, shell = "prose") {
return {
schema: "present.v1",
shell: isPresentShellId(shell) ? shell : "prose",
blocks: { body: text ?? "" },
blocks: { body: stripPresentSourceFences(text ?? "") },
meta: { suggested_actions: [] },
};
}
+20 -34
View File
@@ -4,6 +4,7 @@
* 点发送即答复 → 收起本轮卡(卡上无独立发送钮)。
* 产物验收挂载题:选项 + 底栏意见一并随「按意见修改」写回产物;空内容点该钮不等于接受。
* 分页 / 跳过为悬浮控件,不占独立 header/footer 行。
* 点顶栏横条折叠成一行,再点展开。
*
* 能力「默认问题」(id=module-opening) 不是追问:对齐美学纲领开局,走说话面 + openingGuide。
*/
@@ -259,7 +260,7 @@ export function clearQuestionCardState(host, view) {
host._qState = null;
host.hidden = true;
host.innerHTML = "";
host.classList.remove("is-open", "is-expanded", "has-pager", "has-skip");
host.classList.remove("is-open", "is-expanded", "is-collapsed", "has-pager", "has-skip");
host.removeAttribute("role");
host.removeAttribute("aria-label");
}
@@ -275,7 +276,7 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
if (!active?.questions?.length) {
host.hidden = true;
host.innerHTML = "";
host.classList.remove("is-open", "is-expanded", "has-pager", "has-skip");
host.classList.remove("is-open", "is-expanded", "is-collapsed", "has-pager", "has-skip");
host.removeAttribute("role");
host.removeAttribute("aria-label");
host._qDismissed = null;
@@ -290,7 +291,7 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
) {
host.hidden = true;
host.innerHTML = "";
host.classList.remove("is-open", "is-expanded", "has-pager", "has-skip");
host.classList.remove("is-open", "is-expanded", "is-collapsed", "has-pager", "has-skip");
host.removeAttribute("role");
host.removeAttribute("aria-label");
return false;
@@ -305,14 +306,14 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
sessionId: view.id,
key: qKey,
page: 0,
expanded: false,
collapsed: false,
// questionId -> { optionId?, text, otherText? }
answers: {},
drafts: {}, // option key -> edited label
};
if (typeof state.expanded !== "boolean") state.expanded = false;
if (typeof state.collapsed !== "boolean") state.collapsed = false;
host._qState = state;
host.classList.toggle("is-expanded", state.expanded);
host.classList.toggle("is-collapsed", state.collapsed);
const { questions, pageSize, assessment, optional, skipLabel, dismissOnAccept } =
active;
@@ -362,8 +363,13 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
${pagerHtml}
${skipHtml}
</div>
<button type="button" class="qcard-top" data-qtop title="${state.expanded ? "点击收起" : "点击展开"}" aria-label="${state.expanded ? "收起询问卡" : "展开询问卡"}">
<button type="button" class="qcard-top" data-qtop title="${state.collapsed ? "展开追问" : "折叠追问"}" aria-expanded="${state.collapsed ? "false" : "true"}" aria-label="${state.collapsed ? "展开追问" : "折叠追问"}">
<span class="qcard-top-grip" aria-hidden="true"></span>
<span class="qcard-collapsed-meta">${esc(
pageCount > 1
? `追问 ${state.page + 1}/${pageCount} · ${pageQs[0]?.prompt ?? "询问"}`
: `追问 · ${pageQs[0]?.prompt ?? "询问"}`,
)}</span>
</button>
<div class="qcard-body">
${dismissNote}
@@ -414,40 +420,21 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
.join("")}
</div>`;
const setExpanded = (next) => {
state.expanded = next;
host.classList.toggle("is-expanded", next);
const setCollapsed = (next) => {
state.collapsed = next;
host.classList.toggle("is-collapsed", next);
const top = host.querySelector("[data-qtop]");
if (top) {
top.title = next ? "点击收起" : "点击展开";
top.setAttribute("aria-label", next ? "收起询问卡" : "展开询问卡");
top.title = next ? "展开追问" : "折叠追问";
top.setAttribute("aria-expanded", next ? "false" : "true");
top.setAttribute("aria-label", next ? "展开追问" : "折叠追问");
}
};
host.querySelector("[data-qtop]")?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
setExpanded(!state.expanded);
});
host.querySelector(".qcard-body")?.addEventListener("click", (e) => {
const t = e.target;
if (!(t instanceof Element)) return;
if (
t.closest(
".qcard-letter, .qcard-other-input, [contenteditable=true], .qcard-nav, .qcard-skip, .qcard-pager",
)
) {
return;
}
// 展开后点题干 / 评估区收起;收起时点内容区展开
if (state.expanded) {
if (t.closest(".qcard-prompt, .qcard-assessment, .qcard-optional-note")) {
setExpanded(false);
}
return;
}
setExpanded(true);
setCollapsed(!state.collapsed);
});
host.querySelector("[data-qskip]")?.addEventListener("click", () => {
@@ -488,7 +475,6 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
section.querySelectorAll(".qcard-opt").forEach((li) => {
li.classList.toggle("is-selected", li.getAttribute("data-oid") === oid);
});
if (!state.expanded) setExpanded(true);
_handlers.onAnswersChange?.();
});
});
+220
View File
@@ -800,6 +800,226 @@ code {
background: rgba(24, 24, 27, 0.4);
}
/* —— Preset probe —— */
.probe-dialog {
border: none;
border-radius: 10px;
padding: 0;
background: var(--st-surface);
color: var(--st-text);
width: min(1080px, calc(100% - 24px));
height: min(780px, calc(100% - 24px));
max-width: none;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
}
.probe-dialog[open] {
display: flex;
flex-direction: column;
overflow: hidden;
}
.probe-dialog::backdrop {
background: rgba(24, 24, 27, 0.45);
}
.probe-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 12px;
border-bottom: 1px solid var(--st-border);
flex-shrink: 0;
}
.probe-head h3 {
margin: 0;
font-size: 16px;
font-weight: 650;
}
.probe-sub {
margin: 4px 0 0;
font-size: 12px;
color: var(--st-muted);
}
.probe-body {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
}
.probe-composer,
.probe-inspect {
min-height: 0;
overflow: auto;
padding: 16px 18px 20px;
}
.probe-composer {
border-right: 1px solid var(--st-border);
display: flex;
flex-direction: column;
gap: 12px;
}
.probe-composer label,
.probe-extras label {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 12px;
color: var(--st-muted);
font-weight: 500;
}
.probe-composer textarea,
.probe-extras textarea {
width: 100%;
resize: vertical;
padding: 9px 12px;
border-radius: 6px;
border: 1px solid var(--st-border-strong);
background: var(--st-bg);
color: var(--st-text);
font-size: 13px;
line-height: 1.5;
}
.probe-composer textarea:focus,
.probe-extras textarea:focus {
outline: none;
border-color: #93c5fd;
box-shadow: 0 0 0 3px var(--st-accent-soft);
background: var(--st-surface);
}
.probe-extras {
border: 1px solid var(--st-border);
border-radius: 8px;
padding: 8px 10px 10px;
background: var(--st-bg);
}
.probe-extras summary {
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: var(--st-text);
margin-bottom: 8px;
}
.probe-extras[open] summary {
margin-bottom: 10px;
}
.probe-composer-actions {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
margin-top: auto;
}
.probe-status {
margin: 0;
font-size: 12px;
color: var(--st-muted);
}
.probe-inspect h4 {
margin: 0 0 10px;
font-size: 12px;
font-weight: 650;
color: var(--st-muted);
letter-spacing: 0.02em;
}
.probe-empty {
margin: 8px 0;
color: var(--st-faint);
font-size: 13px;
}
.probe-messages {
display: flex;
flex-direction: column;
gap: 10px;
}
.probe-msg {
border: 1px solid var(--st-border);
border-radius: 8px;
background: var(--st-bg);
overflow: hidden;
}
.probe-msg header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 10px;
border-bottom: 1px solid var(--st-border);
background: var(--st-surface);
}
.probe-role {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--st-accent);
}
.probe-role.user {
color: #0f766e;
}
.probe-role.assistant {
color: #7c3aed;
}
.probe-index {
font-size: 11px;
color: var(--st-faint);
}
.probe-msg pre,
.probe-reply pre {
margin: 0;
padding: 10px 12px;
white-space: pre-wrap;
word-break: break-word;
font-family: ui-monospace, "Cascadia Code", Consolas, monospace;
font-size: 12px;
line-height: 1.55;
}
.probe-reply {
margin-top: 18px;
}
@media (max-width: 800px) {
.probe-dialog {
height: calc(100% - 16px);
}
.probe-body {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.probe-composer {
border-right: none;
border-bottom: 1px solid var(--st-border);
max-height: 42vh;
}
}
.profile-dialog form {
padding: 22px;
display: flex;
+51
View File
@@ -78,6 +78,57 @@
</main>
</div>
<dialog id="preset-probe-dialog" class="probe-dialog">
<header class="probe-head">
<div>
<h3>试跑预设</h3>
<p class="probe-sub" id="probe-preset-name"></p>
</div>
<button type="button" class="btn-secondary" id="probe-close">关闭</button>
</header>
<div class="probe-body">
<section class="probe-composer">
<label>
本轮输入
<textarea id="probe-message" rows="4" placeholder="发给模型的这一轮"></textarea>
</label>
<details class="probe-extras">
<summary>试跑用设定 / 历史</summary>
<label>
历史前(设定)
<textarea id="probe-lore-before" rows="3"></textarea>
</label>
<label>
对话历史
<textarea id="probe-history" rows="4"></textarea>
</label>
<label>
历史后(状态)
<textarea id="probe-lore-after" rows="3"></textarea>
</label>
<label>
本轮后(可选)
<textarea id="probe-post-turn" rows="2"></textarea>
</label>
</details>
<div class="probe-composer-actions">
<button type="button" class="btn-primary" id="probe-send">发送一轮</button>
<p class="probe-status" id="probe-status" hidden></p>
</div>
</section>
<section class="probe-inspect" aria-live="polite">
<h4>发出的上下文</h4>
<div id="probe-messages" class="probe-messages">
<p class="probe-empty">发送后显示实际拼进请求的消息。</p>
</div>
<div id="probe-reply-wrap" class="probe-reply" hidden>
<h4>模型回复</h4>
<pre id="probe-reply"></pre>
</div>
</section>
</div>
</dialog>
<dialog id="profile-dialog" class="profile-dialog">
<form id="profile-form" method="dialog">
<h3 id="profile-dialog-title">新增 API</h3>
+140
View File
@@ -28,6 +28,29 @@ const sectionStorageEl = document.getElementById("section-storage");
const activeSettingsBarEl = document.getElementById("active-settings-bar");
const settingsToastEl = document.getElementById("settings-toast");
const contextTraceKeepEl = document.getElementById("context-trace-keep");
const probeDialog = document.getElementById("preset-probe-dialog");
const probePresetNameEl = document.getElementById("probe-preset-name");
const probeMessageEl = document.getElementById("probe-message");
const probeLoreBeforeEl = document.getElementById("probe-lore-before");
const probeHistoryEl = document.getElementById("probe-history");
const probeLoreAfterEl = document.getElementById("probe-lore-after");
const probePostTurnEl = document.getElementById("probe-post-turn");
const probeSendEl = document.getElementById("probe-send");
const probeStatusEl = document.getElementById("probe-status");
const probeMessagesEl = document.getElementById("probe-messages");
const probeReplyWrapEl = document.getElementById("probe-reply-wrap");
const probeReplyEl = document.getElementById("probe-reply");
const DEFAULT_PROBE_CONTEXT = {
message: "我推开门。",
loreBefore: "一座雨夜的港口旅馆。柜台点着油灯。",
history: "店员:今晚只剩阁楼那间。\n你:好。",
loreAfter: "时间:深夜。地点:旅馆大厅。",
postTurn: "",
};
let probingPresetId = null;
let probeBusy = false;
async function api(path, options = {}) {
const res = await fetch(path, {
@@ -192,6 +215,7 @@ function renderPresets() {
</div>
<div class="config-card-actions">
${activateButtonHtml(active, p.id, "activate-preset")}
<button type="button" data-action="probe-preset" data-id="${p.id}">试跑</button>
<button type="button" data-action="toggle-preset-entries" data-id="${p.id}">
${expanded ? "收起条目" : "编辑条目"}
</button>
@@ -476,6 +500,8 @@ presetsListEl.addEventListener("click", async (e) => {
try {
if (action === "activate-preset") {
await activatePreset(id);
} else if (action === "probe-preset") {
openProbeDialog(id);
} else if (action === "toggle-preset-entries") {
await togglePresetEntries(id);
} else if (action === "delete-preset") {
@@ -573,6 +599,120 @@ presetFileEl.addEventListener("change", async () => {
}
});
function resetProbeForm() {
probeMessageEl.value = DEFAULT_PROBE_CONTEXT.message;
probeLoreBeforeEl.value = DEFAULT_PROBE_CONTEXT.loreBefore;
probeHistoryEl.value = DEFAULT_PROBE_CONTEXT.history;
probeLoreAfterEl.value = DEFAULT_PROBE_CONTEXT.loreAfter;
probePostTurnEl.value = DEFAULT_PROBE_CONTEXT.postTurn;
probeStatusEl.hidden = true;
probeStatusEl.textContent = "";
probeReplyWrapEl.hidden = true;
probeReplyEl.textContent = "";
probeMessagesEl.innerHTML =
'<p class="probe-empty">发送后显示实际拼进请求的消息。</p>';
}
function openProbeDialog(presetId) {
const preset = state.presets.find((p) => p.id === presetId);
probingPresetId = presetId;
probePresetNameEl.textContent = preset?.name ?? presetId;
resetProbeForm();
probeDialog.showModal();
probeMessageEl.focus();
probeMessageEl.select();
}
function renderProbeMessages(messages) {
if (!messages?.length) {
probeMessagesEl.innerHTML =
'<p class="probe-empty">这次没有拼出任何消息。</p>';
return;
}
probeMessagesEl.innerHTML = messages
.map(
(m, i) => `
<article class="probe-msg">
<header>
<span class="probe-role ${escapeHtml(m.role)}">${escapeHtml(m.role)}</span>
<span class="probe-index">#${i + 1}</span>
</header>
<pre>${escapeHtml(m.content)}</pre>
</article>`,
)
.join("");
}
function setProbeBusy(busy) {
probeBusy = busy;
probeSendEl.disabled = busy;
probeSendEl.textContent = busy ? "发送中…" : "发送一轮";
}
async function sendProbe() {
if (probeBusy || !probingPresetId) return;
const message = probeMessageEl.value.trim();
if (!message) {
probeStatusEl.hidden = false;
probeStatusEl.textContent = "请填写本轮输入";
probeMessageEl.focus();
return;
}
setProbeBusy(true);
probeStatusEl.hidden = false;
probeStatusEl.textContent = "正在拼装上下文…";
probeReplyWrapEl.hidden = true;
try {
const result = await api(`/api/presets/${probingPresetId}/probe`, {
method: "POST",
body: JSON.stringify({
message,
loreBefore: probeLoreBeforeEl.value,
history: probeHistoryEl.value,
loreAfter: probeLoreAfterEl.value,
postTurn: probePostTurnEl.value,
}),
});
renderProbeMessages(result.messages);
if (result.reply) {
probeReplyWrapEl.hidden = false;
probeReplyEl.textContent = result.reasoning
? `${result.reasoning}\n\n——\n\n${result.reply}`
: result.reply;
}
if (result.error) {
probeStatusEl.textContent = `上下文已拼装,生成失败:${result.error}`;
} else if (result.skipReason) {
probeStatusEl.textContent = result.skipReason;
} else if (result.completed) {
probeStatusEl.textContent = `已发送 ${result.messages.length} 条`;
} else {
probeStatusEl.textContent = `已拼装 ${result.messages.length} 条`;
}
} catch (err) {
probeStatusEl.textContent = err.message;
} finally {
setProbeBusy(false);
}
}
document.getElementById("probe-close")?.addEventListener("click", () => {
probeDialog.close();
});
probeSendEl?.addEventListener("click", () => {
sendProbe().catch((err) => showToast(err.message, true));
});
probeMessageEl?.addEventListener("keydown", (e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
sendProbe().catch((err) => showToast(err.message, true));
}
});
document.getElementById("btn-save-context-keep")?.addEventListener("click", async () => {
try {
const n = Number(contextTraceKeepEl?.value ?? 5);
+1 -1
View File
@@ -32,7 +32,7 @@
<p class="st-rail-hint">按「要同时看见什么」选,不是按好看选。</p>
<nav class="shells-list" id="shell-list" role="tablist"></nav>
<p class="st-rail-note">
主任务永远是读正文(常 600~3000 字)。侧栏只换壳;长度/语气在预览顶栏。
主任务永远是读正文(常 500~2000 字)。侧栏只换壳;长度/语气在预览顶栏。
</p>
</aside>
+2 -2
View File
@@ -1,6 +1,6 @@
/**
* 呈现壳预览:主工作面 = 壳画布。
* 按「要同时看见什么」选壳;正文按 600~3000 字撑高滚动。
* 按「要同时看见什么」选壳;正文按 500~2000 字撑高滚动。
*/
import {
PRESENT_SHELL_IDS,
@@ -56,7 +56,7 @@ const TONES = [
const LENGTHS = [
{ id: "short", label: "短" },
{ id: "mid", label: "中 ·800" },
{ id: "long", label: "长 ·2500" },
{ id: "long", label: "长 ·2000" },
];
const BODY_MID = `雨还在下。你把伞往她那边偏了偏,她没说话,只是把购物袋往怀里收紧了一点。水从伞骨边缘连成线,砸在柏油路上,溅起细碎的白点。
+331 -45
View File
@@ -18,8 +18,8 @@
--workspace-pad-x: clamp(24px, 4.5vw, 72px);
--workspace-pad-y: clamp(20px, 3vh, 40px);
--head-h: 48px;
--composer-min: 56px;
--composer-control-h: 40px;
--composer-min: 0px;
--composer-control-h: 36px;
--font-ui: "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI", system-ui, sans-serif;
--font-read: "Noto Serif SC", "Songti SC", "STSong", "Source Han Serif SC", Georgia, serif;
font-family: var(--font-ui);
@@ -201,7 +201,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
/* composer 按内容自适应;中间区吃掉剩余高度,整页锁在视口内 */
.panel-main {
display: grid;
grid-template-rows: var(--head-h) minmax(0, 1fr) auto;
grid-template-rows: minmax(0, 1fr) auto;
min-width: 0;
min-height: 0;
height: 100%;
@@ -435,7 +435,7 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
gap: 8px;
}
.coord-empty {
margin: 16px 8px;
@@ -443,6 +443,55 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
color: var(--muted);
text-align: center;
}
.coord-pack {
position: relative;
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px 4px 6px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg);
min-width: 0;
cursor: context-menu;
}
.coord-pack:hover {
border-color: color-mix(in srgb, var(--accent) 28%, var(--border));
background: color-mix(in srgb, var(--accent-soft) 28%, var(--bg));
}
.coord-pack--pending {
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
}
.coord-pack-head {
display: flex;
align-items: center;
min-height: 24px;
padding: 2px 28px 2px 8px;
}
.coord-pack-meta {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--muted);
}
.coord-pack--pending .coord-pack-meta {
color: var(--accent);
}
.coord-pack > .coord-line-menu {
top: 4px;
right: 4px;
}
.coord-pack:hover > .coord-line-menu,
.coord-pack:focus-within > .coord-line-menu {
opacity: 1;
}
.coord-pack .coord-line {
background: transparent;
padding: 6px 8px;
}
.coord-pack .coord-line:hover {
background: color-mix(in srgb, var(--accent-soft) 35%, transparent);
}
.coord-line {
position: relative;
display: flex;
@@ -565,6 +614,10 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
color: var(--accent);
font-weight: 700;
}
.coord-context-request {
color: var(--text);
font-weight: 600;
}
.coord-trace-meta {
display: flex;
flex-wrap: wrap;
@@ -596,6 +649,14 @@ body.design-workspace .workspace-pending {
margin-bottom: 6px;
font-size: 12px;
}
.workspace-pending-head .msg-action {
margin-left: auto;
}
.msg-live-empty {
margin: 0;
color: var(--muted);
font-size: 13px;
}
body.is-reviewing .message-feed {
flex: 0 1 auto;
@@ -752,6 +813,33 @@ body.design-workspace[data-design-surface="answer"] .questions-card-host {
margin-top: 0;
padding: 0;
}
/* 答题/验收工作面已是 surface,询问不再套圆角卡片 */
body.design-workspace[data-design-surface="answer"] .questions-card-host.is-open:not([hidden]),
.workspace-review-ask .questions-card-host.is-open:not([hidden]) {
margin: 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
}
body.design-workspace[data-design-surface="answer"] .qcard-body,
.workspace-review-ask .qcard-body {
padding-left: 0;
padding-right: 0;
}
body.design-workspace[data-design-surface="answer"] .qcard-top,
.workspace-review-ask .qcard-top,
body.design-workspace[data-design-surface="answer"] .questions-card-host.is-collapsed .qcard-top,
.workspace-review-ask .questions-card-host.is-collapsed .qcard-top {
padding-left: 0;
padding-right: 0;
}
body.design-workspace[data-design-surface="answer"] .qcard-skip,
.workspace-review-ask .qcard-skip,
body.design-workspace[data-design-surface="answer"] .qcard-pager,
.workspace-review-ask .qcard-pager {
right: 0;
}
body.design-workspace[data-design-surface="speak"] .workspace-surface-kicker {
color: var(--accent);
}
@@ -766,25 +854,23 @@ body.design-workspace[data-design-surface="review"][data-review-kind="opening"]
color: var(--accent);
}
/* 挂载追问嵌在产物验收面下方,与产物同一工作面 */
/* 挂载追问嵌在产物验收面下方:与产物同宽,随主柱动态适应 */
.workspace-review-ask {
flex: 0 0 auto;
margin-top: 0;
min-height: 0;
max-width: var(--workspace-max, 920px);
width: 100%;
align-self: center;
padding: 0 var(--workspace-pad-x, 24px) var(--workspace-pad-y, 16px);
max-width: none;
align-self: stretch;
padding: 8px var(--workspace-pad-x, 24px) var(--workspace-pad-y, 16px);
box-sizing: border-box;
border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.workspace-review-ask .questions-card-host {
padding-left: 0;
padding-right: 0;
border-top: none;
}
.workspace-review-ask .questions-card-host.is-open:not([hidden]) {
margin-top: 0;
}
body.design-workspace .ws-creation-flow h4 { display: none; }
body.design-workspace .flow-brief {
@@ -1058,6 +1144,32 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
border-radius: 6px;
min-width: 0;
}
.explorer-row.has-twist {
grid-template-columns: 16px minmax(0, 1fr) auto;
}
.explorer-twist {
grid-column: 1;
grid-row: 1 / span 2;
align-self: center;
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
line-height: 1;
color: var(--muted);
user-select: none;
}
.explorer-row.has-twist .explorer-main { grid-column: 2; }
.explorer-row.has-twist .explorer-del { grid-column: 3; }
.explorer-row.child {
padding-left: 28px;
min-height: 28px;
padding-top: 5px;
padding-bottom: 5px;
}
.explorer-row.child .explorer-name { font-size: 12px; font-weight: 400; }
.explorer-row:hover { background: var(--bg); }
.explorer-row.active { background: var(--accent-soft); }
.explorer-row.active .explorer-name { color: var(--accent); font-weight: 600; }
@@ -1119,7 +1231,7 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
color: var(--muted); letter-spacing: 0.04em;
}
.explorer-hint {
margin: 4px 8px; font-size: 11px; color: var(--muted); line-height: 1.4;
margin: 4px 8px 4px 28px; font-size: 11px; color: var(--muted); line-height: 1.4;
}
.sidebar-empty { padding: 12px; color: var(--muted); font-size: 13px; text-align: center; }
@@ -1214,6 +1326,9 @@ body.rail-collapsed .message-feed {
font-size: 11px;
flex-wrap: wrap;
}
.msg-head [data-act="retry_run"] {
margin-left: auto;
}
.msg-foot {
display: flex;
gap: 8px;
@@ -1339,6 +1454,41 @@ body.rail-collapsed .message-feed {
max-height: 240px;
overflow: auto;
}
.msg-play-process-row {
margin: 4px 0;
}
.msg-play-process {
font-size: 12px;
color: var(--muted);
border: 1px dashed var(--border);
border-radius: 6px;
background: #fafafa;
}
.msg-play-process > summary {
cursor: pointer;
display: flex;
align-items: baseline;
gap: 8px;
padding: 6px 10px;
user-select: none;
list-style: none;
}
.msg-play-process > summary::-webkit-details-marker { display: none; }
.msg-play-process > summary::before {
content: "▸";
font-size: 10px;
color: var(--muted);
}
.msg-play-process[open] > summary::before { content: "▾"; }
.msg-play-process .msg-tag {
font-weight: 600;
color: var(--muted);
}
.msg-play-process .msg-body {
padding: 0 10px 10px;
max-height: 320px;
overflow: auto;
}
.msg-live-indicator {
margin-left: 8px;
@@ -1519,7 +1669,7 @@ body.rail-collapsed .message-feed {
position: absolute;
right: 0;
top: calc(100% + 4px);
z-index: 40;
z-index: 80;
min-width: 132px;
padding: 4px;
border: 1px solid var(--border);
@@ -1530,13 +1680,28 @@ body.rail-collapsed .message-feed {
flex-direction: column;
gap: 1px;
}
.composer .more-menu-panel {
top: auto;
bottom: calc(100% + 4px);
}
.more-menu-item {
display: block;
width: 100%;
padding: 7px 10px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text);
text-decoration: none;
text-align: left;
font: inherit;
font-size: 13px;
cursor: pointer;
}
.more-menu-item[hidden] { display: none !important; }
.more-menu-item:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.more-menu-item:hover {
background: var(--bg);
@@ -1567,27 +1732,50 @@ body.rail-collapsed .message-feed {
.msg-q-list li { margin: 0.35rem 0; }
.msg-q-lead { margin: 0; line-height: 1.55; font-size: 14px; }
/* 游玩:阅读柱略抬权重 */
/* 游玩:每轮终稿仍是气泡(方便扫上下文),正文在气泡里撑高 */
body[data-lifecycle="play"] .message-feed {
padding: 14px 18px 18px;
gap: 14px;
padding: 16px 18px 22px;
gap: 16px;
}
body[data-lifecycle="play"] .msg-assistant-row .msg-bubble {
width: min(100%, 44rem);
max-width: 100%;
border-color: color-mix(in srgb, var(--border) 70%, transparent);
box-shadow: none;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
body[data-lifecycle="play"] .msg-assistant-row .msg-head {
padding-top: 6px;
}
body[data-lifecycle="play"] .present-shell {
border-radius: 10px;
}
body[data-lifecycle="play"] .msg-user-row .msg-bubble {
width: min(100%, 28rem);
max-width: 90%;
}
body[data-lifecycle="play"] .msg-play-final .msg-bubble {
width: min(100%, 46rem);
}
body[data-lifecycle="play"] .msg-play-final .present-shell {
border: none;
box-shadow: none;
background: transparent;
padding: 0;
border-radius: 0;
}
body[data-lifecycle="play"] .msg-play-final .present-shell-badge {
display: none;
}
body[data-lifecycle="play"] .msg-play-final .present-stage .present-region--body .present-region-body,
body[data-lifecycle="play"] .msg-play-final .present-stage.present-region--body .present-region-body {
font-size: 18.5px;
line-height: 1.88;
max-width: none;
max-height: none;
overflow: visible;
}
body[data-lifecycle="play"] .msg-play-final .present-shell--chat_monitor .present-stage {
padding: 0;
border: none;
background: transparent;
}
/* 产物紧凑卡:横向主轴,预览一行 */
.artifact-compact-list {
@@ -2455,7 +2643,8 @@ body.design-workspace .af-mosaic {
color: var(--text);
line-height: 1.45;
}
.propose-sync-hint {
.propose-sync-hint,
.propose-plan-hint {
margin: 0;
font-size: 12px;
color: var(--muted);
@@ -2544,6 +2733,9 @@ body.design-workspace .af-mosaic {
font-size: 12px;
font-weight: 400;
}
.flow-occ-revise {
color: var(--text, #222);
}
.flow-order {
grid-row: 1 / -1;
align-self: center;
@@ -2685,10 +2877,9 @@ body.design-workspace .af-mosaic {
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #2a6) 20%, transparent);
}
/* 询问卡:host 本身即卡片(不再套内层 .qcard) */
/* 询问卡:对话流里 host 即卡片;创作工作面上剥壳,见上方 flatten 规则 */
.questions-card-host {
--qcard-max: 48vh;
--qcard-max-expanded: 82vh;
flex: 0 1 auto;
width: 100%;
box-sizing: border-box;
@@ -2710,16 +2901,17 @@ body.design-workspace .af-mosaic {
border: 1px solid var(--border);
border-radius: 10px;
background: var(--surface, #fff);
transition: max-height 0.22s ease;
transition: max-height 0.18s ease;
}
.questions-card-host.is-open.is-expanded:not([hidden]) {
max-height: var(--qcard-max-expanded);
.questions-card-host.is-open.is-collapsed:not([hidden]) {
max-height: none;
}
.qcard-top {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 18px;
margin: 0;
padding: 4px 12px 0;
@@ -2734,18 +2926,40 @@ body.design-workspace .af-mosaic {
.qcard-top-grip {
width: 28px;
height: 3px;
flex-shrink: 0;
border-radius: 999px;
background: color-mix(in srgb, var(--muted, #888) 35%, transparent);
}
.questions-card-host.is-expanded .qcard-top {
min-height: 22px;
padding-top: 6px;
}
.questions-card-host.is-expanded .qcard-top:hover .qcard-top-grip {
.qcard-top:hover .qcard-top-grip {
background: color-mix(in srgb, var(--muted, #888) 55%, transparent);
}
.questions-card-host:not(.is-expanded) .qcard-body {
cursor: pointer;
.qcard-collapsed-meta {
display: none;
min-width: 0;
flex: 1;
font-size: 12px;
font-weight: 550;
line-height: 1.35;
color: var(--text);
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.questions-card-host.is-collapsed .qcard-top {
min-height: 44px;
padding: 0 12px;
justify-content: flex-start;
}
.questions-card-host.is-collapsed .qcard-top:hover .qcard-top-grip {
background: color-mix(in srgb, var(--muted, #888) 55%, transparent);
}
.questions-card-host.is-collapsed .qcard-collapsed-meta {
display: block;
}
.questions-card-host.is-collapsed .qcard-body,
.questions-card-host.is-collapsed .qcard-float {
display: none;
}
/* 悬浮控件层:不占文档流高度 */
.qcard-float {
@@ -3472,6 +3686,7 @@ body.design-workspace .af-mosaic {
border: none; background: transparent; padding: 8px 12px;
font-size: 13px; cursor: pointer; border-radius: 4px;
}
.context-menu-item[hidden] { display: none; }
.context-menu-item:hover { background: var(--bg); }
.context-menu-separator-before { border-top: 1px solid var(--border); }
.context-menu-danger { color: var(--danger); }
@@ -3491,18 +3706,46 @@ body.design-workspace .af-mosaic {
.timeline-item { padding: 6px 0; border-bottom: 1px solid var(--border); }
.timeline-empty { color: var(--muted); }
/* Composer — 单层输入壳;底栏本身不再套第二层卡片 */
/* Composer — 单行:状态 / 输入 / 更多;菜单向上弹出不被裁切 */
.composer {
border-top: 1px solid var(--border);
padding: 10px var(--workspace-pad-x, 16px) 12px;
padding: 6px var(--workspace-pad-x, 16px) 8px;
min-height: var(--composer-min);
max-height: min(50vh, 420px);
overflow: hidden;
overflow: visible;
background: var(--bg);
flex-shrink: 0;
position: relative;
z-index: 20;
display: flex;
flex-direction: column;
justify-content: flex-end;
flex-direction: row;
align-items: flex-end;
gap: 8px;
}
.composer > .status {
flex: 0 0 auto;
align-self: center;
min-width: 0;
}
.composer:has(.composer-mode-chip) > .status {
display: none;
}
#composer-main {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
max-height: min(50vh, 420px);
overflow: auto;
}
.composer > .more-menu {
flex: 0 0 auto;
align-self: flex-end;
margin-bottom: 2px;
}
.composer .more-menu-sum {
height: calc(var(--composer-control-h) - 8px);
display: inline-flex;
align-items: center;
box-sizing: border-box;
}
/* 唯一描边:输入 + 操作同框 */
.composer-input-shell {
@@ -3584,7 +3827,7 @@ body.design-workspace .af-mosaic {
overflow-y: hidden;
border: none;
border-radius: 0;
padding: 8px 4px;
padding: 6px 4px;
line-height: 1.5;
background: transparent;
box-shadow: none;
@@ -3603,7 +3846,16 @@ body.design-workspace .af-mosaic {
margin-right: 2px;
}
.composer-waiting, .composer-idle {
padding: 12px; text-align: center; color: var(--muted); font-size: 13px;
padding: 6px 0; text-align: left; color: var(--muted); font-size: 13px;
}
.composer-waiting {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.composer-waiting .btn {
flex-shrink: 0;
}
.composer-hint { margin: 0 0 8px; font-size: 12px; color: var(--muted); }
.composer-form {
@@ -3656,7 +3908,7 @@ body.design-workspace .af-mosaic {
overflow-y: hidden;
border: none;
border-radius: 0;
padding: 8px 4px;
padding: 6px 4px;
line-height: 1.5;
background: transparent;
field-sizing: content;
@@ -3666,7 +3918,7 @@ body.design-workspace .af-mosaic {
border: none;
box-shadow: none;
}
.composer-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.composer-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 0; }
.composer-actions .btn-primary { flex: 1; }
/* Intake (from intake-ui.js) */
@@ -3729,7 +3981,7 @@ body.design-workspace .af-mosaic {
}
/* —— 游玩呈现壳 present.v1 ——
* 主工作面 = body(随 600~3000 字撑高)。
* 主工作面 = body(随 500~2000 字撑高)。
* 视觉:少盒子、少渐变;层级靠字号/间距/对比,不靠装饰抢注意力。
*/
.present-shell {
@@ -3793,7 +4045,7 @@ body.design-workspace .af-mosaic {
white-space: pre-wrap;
font-size: 14px;
}
/* P0:正文舞台 —— 随字数伸展(600~3000 靠外层滚动,不塞进小框) */
/* P0:正文舞台 —— 随字数伸展(500~2000 靠外层滚动,不塞进小框) */
.present-stage {
flex: 1 1 auto;
min-height: 0;
@@ -4085,3 +4337,37 @@ body.design-workspace .af-mosaic {
color: #e7e5e4;
background: #3f3f46;
}
.opening-setup-body {
display: grid;
gap: 12px;
}
.opening-picker {
display: grid;
gap: 8px;
}
.opening-picker-nav {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.opening-picker-btn {
font: inherit;
font-size: 12px;
padding: 4px 10px;
border: 1px solid var(--border);
background: var(--bg-elev, transparent);
color: inherit;
border-radius: 6px;
cursor: pointer;
}
.opening-picker-btn:hover {
border-color: var(--accent);
}
.opening-picker-pos {
font-size: 12px;
color: var(--muted, #888);
min-width: 4.5em;
text-align: center;
}