dsh-plugin-deck-studio 0.2.27 → 0.2.28

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/ui/workbench.html CHANGED
@@ -186,7 +186,7 @@
186
186
  <p class="nd-tip">先定「讲给谁、讲什么、想带走什么」+ 视觉风格。创建后会开出源目录与 9 份文档骨架,然后在对话里继续补资料、改 script,直到可以出图。<br>新项目保存位置:<code id="ndStoragePath">正在读取配置…</code>。<span id="ndStorageNote"></span></p>
187
187
  <label class="field"><span>主题 / 标题</span><input id="ndTitle" required placeholder="例:XX 业务场景的 AI 落地分享"></label>
188
188
  <label class="field"><span>项目目录名(可选,英文小写中划线;留空自动生成)</span><input id="ndDirName" placeholder="例:talk-ai-transformation-6-stages"></label>
189
- <label class="field"><span>页数(占位页 1–40;创建后还能在对话里追加)</span><input id="ndPages" type="number" min="1" max="40" inputmode="numeric" value="6"></label>
189
+ <label class="field"><span>页数(占位页 1–40;创建后还能在对话里追加或插页)</span><input id="ndPages" type="number" min="1" max="40" inputmode="numeric" value="6"></label>
190
190
  <label class="field"><span>内容密度(一页正文 / 画面要求的目标字数;之后可在 deck 工具栏改)</span>
191
191
  <select id="ndDensity">
192
192
  <option value="auto" selected>自动 —— 由 Agent 按内容判断每页长短(不设固定字数)</option>
@@ -426,8 +426,8 @@
426
426
  <button class="modal-close" id="apClose" title="关闭">✕</button>
427
427
  </div>
428
428
  <div class="modal-body">
429
- <p class="engine-note">填完后点「发送到 Chat」:这段会作为一条完整指令发给 Agent,由它调 <code>deck_script_add_page</code> 出「追加页提案」;我确认后才落 revision(V1 只支持追加到最后一页之后,避免打乱既有页的版本目录)。</p>
430
- <div class="field"><label>位置(V1 只支持追加到最后一页之后)</label><select id="apAfter"></select></div>
429
+ <p class="engine-note">填完后点「发送到 Chat」:这段会作为一条完整指令发给 Agent——追加到末尾调 <code>deck_script_add_page</code>,插到中间或开场位调 <code>deck_script_insert_page</code>;Agent 先出提案,我确认后才落 revision。插页会重排页号,既有页连同已出的图一起后移(图跟着内容走),<b>不需要重出整份</b>。</p>
430
+ <div class="field"><label>插入位置(默认追加到最后一页之后;也可选任意一页之后,或 P01 之前当新的开场位)</label><select id="apAfter"></select></div>
431
431
  <div class="field"><label>页面标题(必填)</label><input id="apTitle" placeholder="例:一张工作观察卡,把五个问题变成检查栏"></div>
432
432
  <div class="field"><label>这一页要回答什么(一句话主张)</label><input id="apClaim" placeholder="例:先看见一段工作,再决定 AI 应该进入哪一步"></div>
433
433
  <div class="field"><label>页面内容 / 结构(一条一行,会原样变成该页正文)</label><textarea id="apBody" rows="6" placeholder="例:
@@ -2770,7 +2770,9 @@ function renderDoc(doc, index) {
2770
2770
  ? `<div class="review-panel"><p class="proposal-strip-note">评审意见 <b>${reviewBatch.length}</b> 条 · 勾选后点「接受并更新」合成<b>一条</b> revision(旧版保留,可回滚);不点就什么都不改</p>${reviewBatch.map(reviewRow).join("")}<div class="review-actions"><button class="btn solid" data-accept-review="1">接受并更新所选</button><span class="review-hint">A=原文可定位;B=转述或支撑偏弱;D=原文在但推不出结论(默认不勾,只作参考);C=无法核实,不进清单</span></div></div>`
2771
2771
  : "";
2772
2772
  return head + styleNote + densityNote + briefBanner + docIssues(doc) + storyOverview + banner + reviewPanel + `<div class="page-cards">${pages.map((page) => {
2773
- const pageProposals = pendingByPage.get(Number(page.page)) || [];
2773
+ // 追加页 / 插页是整份结构提案(kind=script-add-page / script-insert-page),没有「当前值 → 建议值」单值,
2774
+ // 不进页内提案行;它们的 diff 在顶部 banner + 观察页里确认。
2775
+ const pageProposals = (pendingByPage.get(Number(page.page)) || []).filter((proposal) => proposal.kind !== "script-add-page" && proposal.kind !== "script-insert-page");
2774
2776
  const slide = slideById.get(page.id) || {};
2775
2777
  // 已落版值(manifest)优先:确认过的 title/question/body 必须立刻显示,不能被源文档快照盖住。
2776
2778
  const pageTitle = slide.title || page.title;
@@ -2778,8 +2780,8 @@ function renderDoc(doc, index) {
2778
2780
  const bodyText = String(slide.body || page.content || "").trim();
2779
2781
  // visualBrief(manifest)才是出图真正用的提示词;prompts/Pxx.txt 是源侧旧文件。
2780
2782
  const promptText = slide.visualBrief || page.prompt || "";
2781
- const fieldProposals = pageProposals.filter((proposal) => proposal.field !== "script-add-page");
2782
- const staleForPage = stalePending.filter((proposal) => Number(proposal.page) === Number(page.page));
2783
+ const fieldProposals = pageProposals.filter((proposal) => proposal.kind !== "script-add-page" && proposal.kind !== "script-insert-page");
2784
+ const staleForPage = stalePending.filter((proposal) => Number(proposal.page) === Number(page.page) && proposal.kind !== "script-add-page" && proposal.kind !== "script-insert-page");
2783
2785
  const expiredForPage = pageProposals.filter((proposal) => proposal.stale);
2784
2786
  // 「两套」问题的修法:卡片上半部分明确是当前已落版内容,提案行显示「当前值 → 建议值」。
2785
2787
  const proposalBlock = pageProposals.length
@@ -2987,7 +2989,7 @@ const stripFrontmatter = (text) => String(text ?? "").replace(/^---\n[\s\S]*?\n-
2987
2989
  const docFileUrl = (docId, name) => `/api/deck-studio.docfile?` + new URLSearchParams({ deckId: STATE.data.active.deckId, docId, name });
2988
2990
 
2989
2991
  /** F-108:提案字段的展示名 + 预览值(分节提案的 after 是 `**字段**:值` 整行)。 */
2990
- const PROPOSAL_FIELD_LABEL = { title: "标题", question: "副标题", claim: "主张", body: "正文", visualBrief: "画面要求", "script-add-page": "追加页" };
2992
+ const PROPOSAL_FIELD_LABEL = { title: "标题", question: "副标题", claim: "主张", body: "正文", visualBrief: "画面要求", "add-page": "追加页", "insert-page": "插入页", "script-add-page": "追加页", "script-insert-page": "插入页" };
2991
2993
  function proposalFieldLabel(field) {
2992
2994
  return PROPOSAL_FIELD_LABEL[field] || field || "修改";
2993
2995
  }
@@ -3182,7 +3184,8 @@ function bindDocActions(root) {
3182
3184
  root.querySelectorAll("[data-reject-proposal]").forEach((btn) => btn.addEventListener("click", () => decideProposals([btn.dataset.rejectProposal], false)));
3183
3185
  root.querySelectorAll("[data-confirm-page]").forEach((btn) => btn.addEventListener("click", () => {
3184
3186
  const page = Number(btn.dataset.confirmPage);
3185
- const ids = latestByField(deckPendingProposals().filter((proposal) => Number(proposal.page) === page && !proposal.stale)).map((proposal) => proposal.proposalId);
3187
+ // 结构提案(追加页 / 插页)不挂在页卡的「当前值 → 建议值」行上,不能被「这一页全部确认」顺手带走。
3188
+ const ids = latestByField(deckPendingProposals().filter((proposal) => Number(proposal.page) === page && !proposal.stale && proposal.kind !== "script-add-page" && proposal.kind !== "script-insert-page")).map((proposal) => proposal.proposalId);
3186
3189
  decideProposals(ids, true);
3187
3190
  }));
3188
3191
  root.querySelectorAll("[data-confirm-all]").forEach((btn) => btn.addEventListener("click", () => decideProposals(freshProposalIds(), true)));
@@ -4145,11 +4148,20 @@ async function sendAgentPrompt() {
4145
4148
  state.textContent = "正在发送到左侧对话…";
4146
4149
  try {
4147
4150
  await ensurePromptConversation();
4151
+ // F-211:先把「该显示哪个会话」推给宿主 Dock,再发——否则消息到了、左侧还停在旧会话。
4152
+ publishChatSessionToHost(true);
4148
4153
  const result = await requestNativeChat(text);
4149
4154
  if (!result.ok) {
4150
4155
  state.textContent = `未能直接发送${result.reason ? `(${result.reason})` : ""} · 可复制后手动粘贴`;
4151
4156
  return;
4152
4157
  }
4158
+ // F-211:核对落点。宿主要么发进我们给的会话,要么如实回它发去了哪;不一致就别谎报「已发送」。
4159
+ const want = currentChatSessionId();
4160
+ const landed = result.sessionId || "";
4161
+ if (landed && want && landed !== want) {
4162
+ state.textContent = `已发送到另一个会话(${landed.replace(/^session-/, "").slice(0, 8)}…),不是左侧这个;用左上「会话 ▾」切过去,或复制后手动粘贴`;
4163
+ return;
4164
+ }
4153
4165
  state.textContent = "已发送到左侧对话 ✓";
4154
4166
  setTimeout(closeAgentPrompt, 900);
4155
4167
  } catch (error) {
@@ -4855,7 +4867,7 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
4855
4867
  description: pickedDeckStyle()?.description || getEl("ndStyle").value.trim(),
4856
4868
  };
4857
4869
  // 0.2.18:探索式只提交「标题 + 风格」——被隐藏字段里可能还留着旧值,不能偷偷带上;
4858
- // 页数先占 1 页占位,等对话里把故事线聊清楚再追加。
4870
+ // 页数先占 1 页占位,等对话里把故事线聊清楚再追加或插页。
4859
4871
  const payload = explore
4860
4872
  ? { title: getEl("ndTitle").value.trim(), mode: "explore", pages: 1, style }
4861
4873
  : {
@@ -4897,14 +4909,14 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
4897
4909
  `1) 先一次问完 3–5 个关键问题(讲给谁 / 听完要带走什么 / 什么场合和时长 / 手上有没有现成材料 / 有没有必须避开的边界),别一条一条挤牙膏;\n` +
4898
4910
  `2) 我回答后,用 deck_doc_update(docId=intent)把听众 / 带走 / 场合 / 成功标准 / 不讲的写回去;没依据的字段写成「待确认:缺依据,已列问题」,不要编,也不要留模板占位;\n` +
4899
4911
  `3) 再给一版故事线草案:分几段、每段回答什么、大概几页(不确定就给区间),等我确认;\n` +
4900
- `4) 我确认后再用 deck_script_add_page / 提案改 script;要出图我会明确说。\n` +
4912
+ `4) 我确认后再用 deck_script_add_page(末尾加页)/ deck_script_insert_page(插到某页之前,既有页图跟着内容后移、不用重出整份)/ 提案改 script;要出图我会明确说。\n` +
4901
4913
  `材料里没有的数字 / 客户 / 案例 / URL 不要编。`,
4902
4914
  "探索式新建 · 首条消息发给 Agent",
4903
4915
  );
4904
4916
  } else {
4905
4917
  sendPrompt(
4906
4918
  `我刚新建了 deck「${label}」(${deckId})。\n` +
4907
- `页数:已按 ${payload.pages} 页占位(P01–P${String(payload.pages).padStart(2, "0")});页数要改就在对话里说,用 deck_script_add_page 追加。\n` +
4919
+ `页数:已按 ${payload.pages} 页占位(P01–P${String(payload.pages).padStart(2, "0")});页数要改就在对话里说:末尾加页用 deck_script_add_page,插到中间/开场位用 deck_script_insert_page(at=插到第几页之前,既有页图跟着内容后移、不用重出整份)。\n` +
4908
4920
  `受众:${payload.audience || "待确认"};话题:${payload.topic || "待确认"};\n` +
4909
4921
  `意图/希望带走:${payload.intent || "待确认"};\n` +
4910
4922
  `场合/时长:${payload.occasion || "(未填,请根据意图判断或问我)"};` +
@@ -4919,7 +4931,7 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
4919
4931
  `2) 分析每份资料:出处 / 类型 / 能引用的事实 / 能支撑哪条主张 / 还缺什么 → 用 deck_doc_update(docId=materials)写进「支撑材料」;\n` +
4920
4932
  `3) 把 intent 里空着的「场合 / 时长」「成功标准」「不讲的」按意图 + 资料做判断补全 → 用 deck_doc_update(docId=intent)写回;没依据的字段不要编,列成问题问我,并把该字段写成「待确认:缺依据,已列问题」,不要留模板占位;\n` +
4921
4933
  `4) 需要时再用 deck_doc_update(docId=research)补调研条目。\n` +
4922
- `5) 页数不够/多了:用 deck_script_add_page 追加页(先提案、我确认后再 deck_revision_confirm);不要直接改 script.md,也不要回答「工具不支持加页」。\n` +
4934
+ `5) 页数不够/多了:末尾加页用 deck_script_add_page,中间/开场插页用 deck_script_insert_page(at=插到第 N 页之前;既有页号与已出图整体后移、图跟着内容走,不用重出整份)(先提案、我确认后再 deck_revision_confirm);不要直接改 script.md,也不要回答「工具不支持加页/不支持插页」或建议我重出整份。\n` +
4923
4935
  `材料里没有的数字 / 客户 / 案例 / URL 不要编。做完复述你补了什么、还缺什么,再等我确认后起 script 草案,不要直接出图。`,
4924
4936
  "新建 deck · 下一步发给 Agent",
4925
4937
  );
@@ -5246,14 +5258,34 @@ getEl("renderBtn").addEventListener("click", () => renderSlidePrompt(STATE.selec
5246
5258
  /* ---------------- 添加页面:结构化弹层 → 发到 DSH 原生 Chat ---------------- */
5247
5259
 
5248
5260
  const ADD_PAGE = { open: false };
5261
+ /** 页号:manifest 的 slide.order 是 1-based 页号(插页确认后会整体重排)。 */
5262
+ function slidePageNumber(slide, index) {
5263
+ const order = Number(slide?.order);
5264
+ return Number.isFinite(order) && order >= 1 ? order : index + 1;
5265
+ }
5266
+ /** 「添加一页」的位置选项:默认追加末尾 + 每个已有页之后 + P01 之前(开弹窗时按当前页数重算)。 */
5267
+ function addPagePositionOptions(slides) {
5268
+ const list = Array.isArray(slides) ? slides : [];
5269
+ const pad = (value) => String(value).padStart(2, "0");
5270
+ const total = list.length;
5271
+ const options = [
5272
+ `<option value="end" selected>${total ? `追加到最后一页之后(P${pad(total)} 之后)` : "追加到最后一页之后(当前还没有页)"}</option>`,
5273
+ ];
5274
+ list.forEach((slide, index) => {
5275
+ const pageNo = slidePageNumber(slide, index);
5276
+ options.push(`<option value="after:${pageNo}">P${pad(pageNo)} 之后</option>`);
5277
+ });
5278
+ options.push(`<option value="before:1">P01 之前(新的开场位)</option>`);
5279
+ return options.join("");
5280
+ }
5249
5281
  function openAddPageModal() {
5250
5282
  const active = STATE.data?.active;
5251
5283
  if (!active) return;
5252
5284
  const slides = active.slides || [];
5253
5285
  const select = getEl("apAfter");
5254
- select.innerHTML = `<option value="end" selected>最后一页之后(P${String(slides.length).padStart(2, "0")} 之后)</option>`;
5286
+ select.innerHTML = addPagePositionOptions(slides);
5255
5287
  select.value = "end";
5256
- select.disabled = true;
5288
+ select.disabled = false;
5257
5289
  getEl("apTitle").value = "";
5258
5290
  getEl("apClaim").value = "";
5259
5291
  getEl("apBody").value = "";
@@ -5269,10 +5301,39 @@ function closeAddPageModal() {
5269
5301
  getEl("addPageModal").hidden = true;
5270
5302
  ADD_PAGE.open = false;
5271
5303
  }
5304
+ /**
5305
+ * 解析「添加一页」的位置选择:
5306
+ * - end → 追加到末尾(deck_script_add_page)
5307
+ * - after:N → 插到 P0N 之后 = at=N+1(deck_script_insert_page)
5308
+ * - before:1 → 插到 P01 之前 = at=1(deck_script_insert_page,新的开场位)
5309
+ */
5310
+ function readAddPagePosition(slides) {
5311
+ const list = Array.isArray(slides) ? slides : [];
5312
+ const total = list.length;
5313
+ const pad = (value) => String(value).padStart(2, "0");
5314
+ const raw = String(getEl("apAfter").value || "end");
5315
+ if (raw.startsWith("after:")) {
5316
+ const pageNo = Number.parseInt(raw.slice("after:".length), 10);
5317
+ if (Number.isFinite(pageNo) && pageNo >= 1) {
5318
+ return {
5319
+ mode: "insert",
5320
+ at: pageNo + 1,
5321
+ label: `P${pad(pageNo)} 之后(at=${pageNo + 1},即插到第 ${pageNo + 1} 页之前)`,
5322
+ atLast: pageNo >= total,
5323
+ };
5324
+ }
5325
+ }
5326
+ if (raw.startsWith("before:")) {
5327
+ const pageNo = Number.parseInt(raw.slice("before:".length), 10);
5328
+ const at = Number.isFinite(pageNo) && pageNo > 1 ? pageNo : 1;
5329
+ return { mode: "insert", at, label: `P${pad(at)} 之前(at=${at},新的开场位)`, atLast: false };
5330
+ }
5331
+ return { mode: "append", at: total + 1, label: `最后一页(P${pad(total)})之后(追加到末尾)`, atLast: false };
5332
+ }
5272
5333
  function buildAddPagePrompt() {
5273
5334
  const active = STATE.data?.active || {};
5274
5335
  const slides = active.slides || [];
5275
- const afterText = `最后一页(P${String(slides.length).padStart(2, "0")})之后(追加)`;
5336
+ const position = readAddPagePosition(slides);
5276
5337
  const title = getEl("apTitle").value.trim();
5277
5338
  const claim = getEl("apClaim").value.trim();
5278
5339
  const body = getEl("apBody").value.trim();
@@ -5284,7 +5345,7 @@ function buildAddPagePrompt() {
5284
5345
  return "";
5285
5346
  }
5286
5347
  const lines = [
5287
- `请在 deck「${active.title || active.deckId}」(${active.deckId})里新增一页,位置:${afterText}。`,
5348
+ `请在 deck「${active.title || active.deckId}」(${active.deckId})里${position.mode === "insert" ? "插入一页" : "新增一页"},位置:${position.label}。`,
5288
5349
  ``,
5289
5350
  `页面标题:${title}`,
5290
5351
  ];
@@ -5295,13 +5356,21 @@ function buildAddPagePrompt() {
5295
5356
  }
5296
5357
  if (visual) lines.push(``, `画面要求:${visual}`);
5297
5358
  if (prompt) lines.push(``, `出图提示词草稿(可润色):`, `"""`, prompt, `"""`);
5298
- lines.push(
5299
- ``,
5300
- `要求:`,
5301
- `1. 调 deck_script_add_page(deckId=${active.deckId})追加一页:title / question / claim / body / visualBrief 一次给全;不要自己改 script.md(你也没有写文件的工具);`,
5302
- `2. 把工具返回的 diff 贴给我,等我确认后再调 deck_revision_confirm(confirmationQuote 引用我原话);`,
5303
- `3. 新页默认没有图:先补 visualBrief 再出这一页;这一轮只处理新增页,别的页别动。`,
5304
- );
5359
+ lines.push(``, `要求:`);
5360
+ if (position.mode === "insert") {
5361
+ lines.push(
5362
+ `1. 调 deck_script_insert_page(deckId=${active.deckId}, at=${position.at}):pages 数组里给一项 { title, question, claim, body, visualBrief },一次给全;reason 写一句为什么插在这里。不要自己改 script.md(你也没有写文件的工具);`,
5363
+ `2. 插入后既有页的页号会整体后移(页号 >= ${position.at} 的页 +1)${position.atLast ? ";这里 at 已等于末页之后,等价于追加到末尾" : ""};已出的图跟着各自内容走(slides 目录随页迁移),**不需要重出整份,也不要建议我重出整份**;`,
5364
+ `3. 把工具返回的 diff 贴给我,等我确认后再调 deck_revision_confirm(confirmationQuote 引用我原话);`,
5365
+ `4. 新页默认没有图:先补 visualBrief 再出这一页;这一轮只处理新增页,别的页的内容和画面要求别动。`,
5366
+ );
5367
+ } else {
5368
+ lines.push(
5369
+ `1. 调 deck_script_add_page(deckId=${active.deckId}):pages 数组里给一项 { title, question, claim, body, visualBrief },一次给全;reason 写一句为什么加这一页。不要自己改 script.md(你也没有写文件的工具);`,
5370
+ `2. 把工具返回的 diff 贴给我,等我确认后再调 deck_revision_confirm(confirmationQuote 引用我原话);`,
5371
+ `3. 新页默认没有图:先补 visualBrief 再出这一页;这一轮只处理新增页,别的页别动。`,
5372
+ );
5373
+ }
5305
5374
  return lines.join("\n");
5306
5375
  }
5307
5376
  async function sendAddPageToChat() {
@@ -5402,6 +5471,20 @@ function setupNativeChat() {
5402
5471
  * 桌面(dsh-app://app)的对话列:不嵌 DSH 页面,把头部保留成「绑定 + 会话切换」,
5403
5472
  * 动作经宿主桥(client.js)在宿主主区打开原生会话;打开成功后关闭工作台面板。
5404
5473
  */
5474
+ /**
5475
+ * 桌面 Dock:把「本 deck / design 的会话」报给宿主,Dock 据此渲染原生会话。
5476
+ * F-211:从 setupHostChatColumn 里提出来,发送指令前后可以立刻推一次,
5477
+ * 不用等 1.2s 心跳(否则会出现「消息已经到了、左侧还是旧会话」)。
5478
+ */
5479
+ let chatSessionPublished = "§init§";
5480
+ function publishChatSessionToHost(force = false) {
5481
+ if (!DESKTOP_HOST) return;
5482
+ const sessionId = currentChatSessionId();
5483
+ if (!force && sessionId === chatSessionPublished) return;
5484
+ chatSessionPublished = sessionId;
5485
+ try { window.parent?.postMessage({ type: "dsh-deck-studio:chat-session", sessionId }, "*"); } catch { /* 宿主桥不可用 */ }
5486
+ }
5487
+
5405
5488
  function setupHostChatColumn() {
5406
5489
  const column = getEl("chatCol");
5407
5490
  const frame = getEl("nativeChatFrame");
@@ -5418,15 +5501,8 @@ function setupHostChatColumn() {
5418
5501
  if (!STATE.dockActive) getEl("chatHostCard").hidden = false;
5419
5502
  }, 6000);
5420
5503
  // 心跳把当前绑定会话报给宿主:Dock 据此用原生会话盖住 #chatDockArea。
5421
- let lastPublished = "§init§";
5422
- const publishChatSession = () => {
5423
- const sessionId = currentChatSessionId();
5424
- if (sessionId === lastPublished) return;
5425
- lastPublished = sessionId;
5426
- try { window.parent?.postMessage({ type: "dsh-deck-studio:chat-session", sessionId }, "*"); } catch { /* 宿主桥不可用 */ }
5427
- };
5428
- setInterval(publishChatSession, 1200);
5429
- try { window.parent?.postMessage({ type: "dsh-deck-studio:chat-session", sessionId: lastPublished = currentChatSessionId() }, "*"); } catch { /* 同上 */ }
5504
+ setInterval(() => publishChatSessionToHost(), 1200);
5505
+ publishChatSessionToHost(true);
5430
5506
  window.addEventListener("message", (event) => {
5431
5507
  if (event.source !== window.parent) return;
5432
5508
  if (event.data?.type !== "dsh-deck-studio:chat-dock") return;
@@ -5505,21 +5581,34 @@ function nativeChatWindow() {
5505
5581
  function requestNativeChat(text) {
5506
5582
  const target = nativeChatWindow();
5507
5583
  if (!target) return Promise.resolve({ ok: false, reason: "没有可用的 DSH 对话窗口" });
5584
+ /**
5585
+ * F-211:必须把「本 deck / design 的会话」一起送过去,不能让宿主自己挑。
5586
+ * 桌面 Dock 模式下宿主记的 `dsh.sessions.current` 是宿主主区那个会话(见 currentChatSessionId 的注释),
5587
+ * 常常不是左侧 Dock 正在显示的这个——只发 text 会让指令落进隔壁会话:
5588
+ * 弹层提示「已发送到左侧对话 ✓」,左边却什么也没有(2026-11-07 真机复现:
5589
+ * 指令进了未绑定的 session-a274e455,绑定的 session-e2eda8c9 一直空着)。
5590
+ */
5591
+ const sessionId = currentChatSessionId();
5508
5592
  return new Promise((resolve) => {
5509
5593
  let settled = false;
5510
5594
  const onAck = (event) => {
5511
5595
  if (event.data?.type !== "dsh-deck-studio:prompt-ack") return;
5512
5596
  settled = true;
5513
5597
  window.removeEventListener("message", onAck);
5514
- resolve({ ok: Boolean(event.data.ok), reason: event.data.reason || "" });
5598
+ resolve({
5599
+ ok: Boolean(event.data.ok),
5600
+ sessionId: event.data.sessionId || sessionId,
5601
+ reason: event.data.reason || "",
5602
+ });
5515
5603
  };
5516
5604
  window.addEventListener("message", onAck);
5517
- target.postMessage({ type: "dsh-deck-studio:prompt", text }, "*");
5605
+ target.postMessage({ type: "dsh-deck-studio:prompt", text, sessionId }, "*");
5518
5606
  setTimeout(() => {
5519
5607
  if (settled) return;
5520
5608
  window.removeEventListener("message", onAck);
5521
- resolve({ ok: false, reason: "宿主 2.5s 内没有回应" });
5522
- }, 2500);
5609
+ // 宿主 inject 会话上下文(最多 1.5s)再 send,2.5s 太紧,容易「其实发出去了但界面报没回应」。
5610
+ resolve({ ok: false, reason: "宿主 8s 内没有回应" });
5611
+ }, 8000);
5523
5612
  });
5524
5613
  }
5525
5614
 
@@ -5556,6 +5645,8 @@ function requestConversation(payload) {
5556
5645
  sourceRoot: STATE.data?.active?.sourceRoot || "",
5557
5646
  // 0.2.16:宿主要按「deck 自己的项目目录」建会话工作区(桌面里 cwd 是 profile 目录,用不了)。
5558
5647
  sessionWorkspacePath: STATE.data?.active?.sessionWorkspacePath || "",
5648
+ // F-210:宿主拿它给工作区起名(`Deckline · <deck 标题>`),侧栏才像 HTML Deck 一样按 deck 分组。
5649
+ title: STATE.data?.active?.title || "",
5559
5650
  ...payload,
5560
5651
  // F-202B:桌面 Dock 模式默认不抢主区(open:false);显式 takeMain → open:true 切宿主主区。
5561
5652
  // web 不传 open(undefined → 宿主按原语义 openSession)。
@@ -5999,6 +6090,8 @@ function renderChatHeader() {
5999
6090
  const label = getEl("chatSessionLabel");
6000
6091
  const count = getEl("chatSessionCount");
6001
6092
  const target = activeChatTarget();
6093
+ // F-211:头部重画时顺手把「该显示哪个会话」推给宿主 Dock(内部去重,未变化不重复 postMessage)。
6094
+ publishChatSessionToHost();
6002
6095
  if (!target) {
6003
6096
  chip.textContent = "—";
6004
6097
  chip.className = "bind-chip unbound";