dsh-plugin-deck-studio 0.2.13 → 0.2.24

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
@@ -177,6 +177,11 @@
177
177
  <button class="modal-close" id="ndClose" title="关闭">✕</button>
178
178
  </div>
179
179
  <form class="modal-body newdeck-body" id="newDeckForm">
180
+ <div class="nd-mode" id="ndMode" role="radiogroup" aria-label="开工方式">
181
+ <label class="nd-mode-opt"><input type="radio" name="ndMode" value="form" checked><span><b>表单模式</b><em>受众 / 意图 / 页数先定清楚</em></span></label>
182
+ <label class="nd-mode-opt"><input type="radio" name="ndMode" value="explore"><span><b>探索模式</b><em>只有一句模糊想法也能开工,边聊边定</em></span></label>
183
+ </div>
184
+ <p class="engine-note nd-mode-note" id="ndModeNote" hidden>探索模式只留「标题 + 风格」:其余字段先隐藏。建完 Agent 会在对话里一次问清听众 / 想让人带走什么 / 场合 / 材料 / 边界,再给你一版故事线草案——问清之前不出图、不留 TODO。</p>
180
185
  <p class="nd-tip">先定「讲给谁、讲什么、想带走什么」+ 视觉风格。创建后会开出源目录与 9 份文档骨架,然后在对话里继续补资料、改 script,直到可以出图。<br>新项目保存位置:<code id="ndStoragePath">正在读取配置…</code>。<span id="ndStorageNote"></span></p>
181
186
  <label class="field"><span>主题 / 标题</span><input id="ndTitle" required placeholder="例:XX 业务场景的 AI 落地分享"></label>
182
187
  <label class="field"><span>项目目录名(可选,英文小写中划线;留空自动生成)</span><input id="ndDirName" placeholder="例:talk-ai-transformation-6-stages"></label>
@@ -286,6 +291,11 @@
286
291
  </div>
287
292
  <div class="chat-session-menu" id="chatSessionMenu" hidden></div>
288
293
  </div>
294
+ <div id="chatDockArea" hidden title="这里由宿主渲染原生会话(桌面 Dock)"></div>
295
+ <div id="chatHostCard" hidden>
296
+ <p class="chat-host-hint">桌面 App 里内嵌 DSH 页面无法启动(子帧拿不到 boot 注入)。宿主原生会话 Dock 没有就绪——对话将<b>在宿主主区</b>打开,点 ☰ 或左侧栏 Deckline 行切回工作台。</p>
297
+ <button type="button" class="btn alt" id="chatHostOpen">在宿主主区打开对话 →</button>
298
+ </div>
289
299
  <iframe id="nativeChatFrame" title="DSH 原生对话"></iframe>
290
300
  </aside>
291
301
  <div class="splitter" data-split="nav" title="拖动调整对话栏宽度" hidden></div>
@@ -583,9 +593,70 @@ let SETTINGS_FORM_DIRTY = false;
583
593
 
584
594
  const STATE = { sid: null, sessionTitle: "", data: null, selected: null, running: false, demo: false, tab: "deck", entityKind: "", sig: "", docCache: {}, docLoading: null, view: "home", deckId: null, rightTab: "", sessions: [], editorPage: null, ndStyleAbbr: "", designCards: [] };
585
595
  const EMBED = new URLSearchParams(location.search).get("embed") === "1";
596
+ /**
597
+ * F-202B:桌面宿主(自定义协议 `dsh-app://app`)的 DSH 子帧拿不到 boot 注入(`__DSH_BOOT_READY__`
598
+ * 只注入主帧),流式端点也在子帧 404 —— `/?decklineChat=1` 嵌套实例永远停在「Loading plugins…」。
599
+ * 桌面上对话改走宿主原生(client.js 的 conversation 桥 → 宿主 uiWorkspace.openSession,会话开在宿主主区);
600
+ * web 版(http 起源,如 13090 / 3080)保持 iframe 内嵌不动。
601
+ */
602
+ const DESKTOP_HOST = !/^(https?:)$/.test(location.protocol)
603
+ || new URLSearchParams(location.search).get("chatHost") === "panel";
586
604
 
587
605
  const esc = (v) => String(v ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
588
606
  const getEl = (id) => document.getElementById(id);
607
+
608
+ /* 0.2.21:宿主「点左侧会话 → 打开这本 deck」的指令常常比本脚本启动还早(面板刚挂载就开始发),
609
+ 旧实现把监听注册在 boot 末尾,消息就直接丢了——表现成「工作区弹出来了,但停在首页」。
610
+ 这里一启动就注册监听:没 boot 完先暂存,boot 完再执行,并回 ack 让宿主确认送达。 */
611
+ const OPEN_BINDING = { pending: null, booted: false };
612
+
613
+ function ackOpenBinding(data, result) {
614
+ try {
615
+ window.parent?.postMessage({ type: "dsh-deck-studio:open-binding-ack", id: data?.id ?? "", kind: data?.kind ?? "", ...result }, "*");
616
+ } catch {
617
+ /* 独立打开(无宿主)时忽略 */
618
+ }
619
+ }
620
+
621
+ function applyOpenBinding(data) {
622
+ if (!data || typeof data !== "object") return;
623
+ if (data.notice) {
624
+ flash(data.notice);
625
+ ackOpenBinding(data, { ok: true, action: "notice" });
626
+ return;
627
+ }
628
+ if (!data.id) return;
629
+ if (data.kind === "design") {
630
+ const project = (STATE.data?.designProjects || []).find((item) => item.designId === data.id);
631
+ const canvasId = project?.canvasDeckId || "";
632
+ if (!canvasId) {
633
+ flash(`找不到这个风格的画布:${data.id}`, "error");
634
+ ackOpenBinding(data, { ok: false, reason: "design canvas not found" });
635
+ return;
636
+ }
637
+ showDeck(canvasId)
638
+ .then(() => ackOpenBinding(data, { ok: true, deckId: canvasId }))
639
+ .catch((error) => {
640
+ flash(`打开风格画布失败:${error?.message ?? error}`, "error");
641
+ ackOpenBinding(data, { ok: false, reason: String(error?.message ?? error) });
642
+ });
643
+ return;
644
+ }
645
+ showDeck(data.id)
646
+ .then(() => ackOpenBinding(data, { ok: true, deckId: data.id }))
647
+ .catch((error) => {
648
+ flash(`打开 deck 失败:${error?.message ?? error}`, "error");
649
+ ackOpenBinding(data, { ok: false, reason: String(error?.message ?? error) });
650
+ });
651
+ }
652
+
653
+ window.addEventListener("message", (event) => {
654
+ if (event.source !== window.parent) return;
655
+ const data = event.data ?? {};
656
+ if (data.type !== "dsh-deck-studio:open-binding") return;
657
+ if (OPEN_BINDING.booted) applyOpenBinding(data);
658
+ else OPEN_BINDING.pending = data;
659
+ });
589
660
  const fmtBytes = (n) => (n ? (n > 1048576 ? (n / 1048576).toFixed(1) + " MB" : Math.round(n / 1024) + " KB") : "");
590
661
  const fmtTime = (iso) => String(iso ?? "").replace("T", " ").replace(/\.\d+Z$/, "Z").replace("Z", "").slice(5);
591
662
 
@@ -1635,8 +1706,8 @@ function renderHomeDecks(decks, active, gov) {
1635
1706
  ${deck.deletable ? `<button type="button" class="design-tool danger deck-tool" data-deck-delete="${esc(deck.deckId)}" title="删除这个 deck(本机新建的才可以)">${TRASH_ICON}</button>` : ""}
1636
1707
  <div class="home-block-media">${homeThumb(deck)}</div>
1637
1708
  <div class="home-block-body">
1638
- <div class="home-block-title">${esc(homeDeckTitle(deck))}${deck.kind === "design-canvas" ? `<span class="pill canvas-pill">风格画布</span>` : ""}</div>
1639
- <div class="home-block-meta">${deck.kind === "design-canvas" ? "风格画布" : `${esc(String(deck.pageCount ?? 0))} 页`}${score != null ? ` · 治理 ${score}/100` : ""}</div>
1709
+ <div class="home-block-title">${esc(homeDeckTitle(deck))}${deck.kind === "design-canvas" ? `<span class="pill canvas-pill">风格画布</span>` : ""}${deck.unmounted ? `<span class="pill canvas-pill" title="这份 deck 的源目录不在了(只剩工作副本):点开在右侧 Inspector 换源 / 重新登记">源未挂载</span>` : ""}</div>
1710
+ <div class="home-block-meta">${deck.unmounted ? "源未挂载 · 只剩工作副本" : deck.kind === "design-canvas" ? "风格画布" : `${esc(String(deck.pageCount ?? 0))} 页`}${score != null ? ` · 治理 ${score}/100` : ""}</div>
1640
1711
  ${styleChip || docTags || rulesChip ? `<div class="home-block-tags">${styleChip}${rulesChip}${docTags}</div>` : ""}
1641
1712
  </div>
1642
1713
  </div>`;
@@ -4644,6 +4715,41 @@ function newDeckStorageInfo(config = {}) {
4644
4715
  return { path: "暂时无法确定", note: "请等待工作台配置加载后重试;未配置 projectsRoot 时使用 <dataRoot>/projects/<deck-id>/source/。" };
4645
4716
  }
4646
4717
 
4718
+ /** 0.2.18:探索模式要藏起来的表单字段(标题 / 风格 / 参考资料之外的「先填清楚」项)。 */
4719
+ const ND_FORM_ONLY = ["ndDirName", "ndPages", "ndDensity", "ndAudience", "ndTopic", "ndIntent", "ndOccasion", "ndSuccess", "ndScopeOut", "ndRefs"];
4720
+
4721
+ /** 当前开工方式:form(表单)/ explore(探索,主意还没定清楚)。 */
4722
+ function newDeckMode() {
4723
+ return document.querySelector('#ndMode input[name="ndMode"]:checked')?.value === "explore" ? "explore" : "form";
4724
+ }
4725
+
4726
+ /**
4727
+ * 切换开工方式:探索式只留「标题 + 风格」,其余字段隐藏(值保留,切回表单还在);
4728
+ * 隐藏的字段不能用 hidden 属性就完事——.newdeck-body .field 是 display:flex,会盖掉浏览器默认样式。
4729
+ */
4730
+ function applyNewDeckMode() {
4731
+ const explore = newDeckMode() === "explore";
4732
+ for (const id of ND_FORM_ONLY) {
4733
+ const field = getEl(id)?.closest(".field");
4734
+ if (!field) continue;
4735
+ field.hidden = explore;
4736
+ field.style.display = explore ? "none" : "";
4737
+ }
4738
+ // 成对的行(场合 / 成功标准)两个字段都藏起来后,行本身也要收掉,否则会留一条空栅格。
4739
+ document.querySelectorAll("#newDeckForm .nd-row").forEach((row) => {
4740
+ const allHidden = [...row.querySelectorAll(".field")].every((field) => field.hidden);
4741
+ row.hidden = explore && allHidden;
4742
+ row.style.display = explore && allHidden ? "none" : "";
4743
+ });
4744
+ const pages = getEl("ndPages");
4745
+ if (pages) pages.required = !explore;
4746
+ getEl("ndModeNote").hidden = !explore;
4747
+ const button = getEl("ndCreate");
4748
+ if (button) button.textContent = explore ? "开始探索 →" : "创建并开始对话";
4749
+ getEl("newDeckModal").dataset.mode = explore ? "explore" : "form";
4750
+ return explore;
4751
+ }
4752
+
4647
4753
  function openNewDeckModal() {
4648
4754
  // F-112:profile 里配了 defaultStyleAbbr 就默认预选这个风格(用户仍可改/取消)。
4649
4755
  STATE.ndStyleAbbr = String(STATE.data?.config?.defaultStyleAbbr || "");
@@ -4653,6 +4759,7 @@ function openNewDeckModal() {
4653
4759
  getEl("ndStoragePath").textContent = storage.path;
4654
4760
  getEl("ndStorageNote").textContent = storage.note;
4655
4761
  getEl("newDeckModal").hidden = false;
4762
+ applyNewDeckMode();
4656
4763
  renderDeckStyleChoices();
4657
4764
  setTimeout(() => getEl("ndTitle").focus(), 30);
4658
4765
  }
@@ -4665,26 +4772,33 @@ getEl("newDeckBtn").addEventListener("click", openNewDeckModal);
4665
4772
  getEl("ndClose").addEventListener("click", closeNewDeckModal);
4666
4773
  getEl("ndCancel").addEventListener("click", closeNewDeckModal);
4667
4774
  getEl("newDeckModal").addEventListener("click", (event) => { if (event.target === getEl("newDeckModal")) closeNewDeckModal(); });
4775
+ getEl("ndMode").addEventListener("change", applyNewDeckMode);
4668
4776
  getEl("newDeckForm").addEventListener("submit", async (event) => {
4669
4777
  event.preventDefault();
4670
- const payload = {
4671
- title: getEl("ndTitle").value.trim(),
4672
- dirName: getEl("ndDirName").value.trim(),
4673
- pages: Math.min(Math.max(Number.parseInt(getEl("ndPages").value, 10) || 6, 1), 40),
4674
- contentDensity: getEl("ndDensity").value || "auto",
4675
- audience: getEl("ndAudience").value.trim(),
4676
- topic: getEl("ndTopic").value.trim(),
4677
- intent: getEl("ndIntent").value.trim(),
4678
- occasion: getEl("ndOccasion").value.trim(),
4679
- success: getEl("ndSuccess").value.trim(),
4680
- scopeOut: getEl("ndScopeOut").value.trim(),
4681
- references: getEl("ndRefs").value.trim(),
4682
- style: {
4683
- abbr: STATE.ndStyleAbbr || "",
4684
- // preset 选中的话,description 仍带完整风格定义(只是不给用户看)
4685
- description: pickedDeckStyle()?.description || getEl("ndStyle").value.trim(),
4686
- },
4778
+ const explore = newDeckMode() === "explore";
4779
+ const style = {
4780
+ abbr: STATE.ndStyleAbbr || "",
4781
+ // preset 选中的话,description 仍带完整风格定义(只是不给用户看)
4782
+ description: pickedDeckStyle()?.description || getEl("ndStyle").value.trim(),
4687
4783
  };
4784
+ // 0.2.18:探索式只提交「标题 + 风格」——被隐藏字段里可能还留着旧值,不能偷偷带上;
4785
+ // 页数先占 1 页占位,等对话里把故事线聊清楚再追加。
4786
+ const payload = explore
4787
+ ? { title: getEl("ndTitle").value.trim(), mode: "explore", pages: 1, style }
4788
+ : {
4789
+ title: getEl("ndTitle").value.trim(),
4790
+ dirName: getEl("ndDirName").value.trim(),
4791
+ pages: Math.min(Math.max(Number.parseInt(getEl("ndPages").value, 10) || 6, 1), 40),
4792
+ contentDensity: getEl("ndDensity").value || "auto",
4793
+ audience: getEl("ndAudience").value.trim(),
4794
+ topic: getEl("ndTopic").value.trim(),
4795
+ intent: getEl("ndIntent").value.trim(),
4796
+ occasion: getEl("ndOccasion").value.trim(),
4797
+ success: getEl("ndSuccess").value.trim(),
4798
+ scopeOut: getEl("ndScopeOut").value.trim(),
4799
+ references: getEl("ndRefs").value.trim(),
4800
+ style,
4801
+ };
4688
4802
  if (!payload.title) return;
4689
4803
  getEl("ndCreate").disabled = true;
4690
4804
  getEl("ndState").textContent = "正在创建 deck 项目(源目录 + 9 份文档骨架)…";
@@ -4703,6 +4817,18 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
4703
4817
  // 停在 home(loadWorkbench 在 home 视图下只会重渲染首页,用户会以为「没自动打开」)。
4704
4818
  await showDeck(deckId);
4705
4819
  getEl("renderStatus").textContent = `已创建 ${label};对话在 DSH 原生 Chat 里继续。`;
4820
+ if (explore) {
4821
+ sendPrompt(
4822
+ `我刚新建了 deck「${label}」(${deckId}),用的是**探索模式**:我一开始只有个模糊想法,没有现成简报。\n` +
4823
+ `请按这个顺序把我带清楚,不要留 TODO、不要出图:\n` +
4824
+ `1) 先一次问完 3–5 个关键问题(讲给谁 / 听完要带走什么 / 什么场合和时长 / 手上有没有现成材料 / 有没有必须避开的边界),别一条一条挤牙膏;\n` +
4825
+ `2) 我回答后,用 deck_doc_update(docId=intent)把听众 / 带走 / 场合 / 成功标准 / 不讲的写回去;没依据的字段写成「待确认:缺依据,已列问题」,不要编,也不要留模板占位;\n` +
4826
+ `3) 再给一版故事线草案:分几段、每段回答什么、大概几页(不确定就给区间),等我确认;\n` +
4827
+ `4) 我确认后再用 deck_script_add_page / 提案改 script;要出图我会明确说。\n` +
4828
+ `材料里没有的数字 / 客户 / 案例 / URL 不要编。`,
4829
+ "探索式新建 · 首条消息发给 Agent",
4830
+ );
4831
+ } else {
4706
4832
  sendPrompt(
4707
4833
  `我刚新建了 deck「${label}」(${deckId})。\n` +
4708
4834
  `页数:已按 ${payload.pages} 页占位(P01–P${String(payload.pages).padStart(2, "0")});页数要改就在对话里说,用 deck_script_add_page 追加。\n` +
@@ -4724,6 +4850,7 @@ getEl("newDeckForm").addEventListener("submit", async (event) => {
4724
4850
  `材料里没有的数字 / 客户 / 案例 / URL 不要编。做完复述你补了什么、还缺什么,再等我确认后起 script 草案,不要直接出图。`,
4725
4851
  "新建 deck · 下一步发给 Agent",
4726
4852
  );
4853
+ }
4727
4854
  } catch (error) {
4728
4855
  getEl("ndState").textContent = "创建失败:" + error.message;
4729
4856
  } finally {
@@ -4898,6 +5025,7 @@ function decorateNativeChat(doc) {
4898
5025
 
4899
5026
  function setupNativeChat() {
4900
5027
  if (EMBED) return; // embed 模式下宿主 shell 本身就有原生 Chat
5028
+ if (DESKTOP_HOST) return setupHostChatColumn(); // F-202B:桌面不嵌 DSH 页面
4901
5029
  const column = getEl("chatCol");
4902
5030
  const frame = getEl("nativeChatFrame");
4903
5031
  const splitter = document.querySelector('.splitter[data-split="nav"]');
@@ -4937,6 +5065,92 @@ function setupNativeChat() {
4937
5065
  });
4938
5066
  }
4939
5067
 
5068
+ /**
5069
+ * 桌面(dsh-app://app)的对话列:不嵌 DSH 页面,把头部保留成「绑定 + 会话切换」,
5070
+ * 动作经宿主桥(client.js)在宿主主区打开原生会话;打开成功后关闭工作台面板。
5071
+ */
5072
+ function setupHostChatColumn() {
5073
+ const column = getEl("chatCol");
5074
+ const frame = getEl("nativeChatFrame");
5075
+ const splitter = document.querySelector('.splitter[data-split="nav"]');
5076
+ if (!column || !frame) return;
5077
+ column.hidden = false;
5078
+ if (splitter) splitter.hidden = false;
5079
+ document.body.classList.add("has-chat");
5080
+ frame.hidden = true;
5081
+ getEl("chatDockArea").hidden = false;
5082
+ STATE.dockActive = false;
5083
+ // 降级卡:宿主原生 Dock 没接上(少见;比如 shell.overlay 契约缺失)时,退回「在主区打开」。
5084
+ const fallbackTimer = setTimeout(() => {
5085
+ if (!STATE.dockActive) getEl("chatHostCard").hidden = false;
5086
+ }, 6000);
5087
+ // 心跳把当前绑定会话报给宿主:Dock 据此用原生会话盖住 #chatDockArea。
5088
+ let lastPublished = "§init§";
5089
+ const publishChatSession = () => {
5090
+ const sessionId = currentChatSessionId();
5091
+ if (sessionId === lastPublished) return;
5092
+ lastPublished = sessionId;
5093
+ try { window.parent?.postMessage({ type: "dsh-deck-studio:chat-session", sessionId }, "*"); } catch { /* 宿主桥不可用 */ }
5094
+ };
5095
+ setInterval(publishChatSession, 1200);
5096
+ try { window.parent?.postMessage({ type: "dsh-deck-studio:chat-session", sessionId: lastPublished = currentChatSessionId() }, "*"); } catch { /* 同上 */ }
5097
+ window.addEventListener("message", (event) => {
5098
+ if (event.source !== window.parent) return;
5099
+ if (event.data?.type !== "dsh-deck-studio:chat-dock") return;
5100
+ STATE.dockActive = true;
5101
+ getEl("chatHostCard").hidden = true;
5102
+ clearInterval(fallbackTimer);
5103
+ });
5104
+ getEl("chatBindChip").addEventListener("click", () => {
5105
+ if (!getEl("chatBindChip").classList.contains("unbound")) return;
5106
+ bindCurrentChatToTarget().catch(() => {});
5107
+ });
5108
+ getEl("newChatBtn").addEventListener("click", () => {
5109
+ newChatForTarget().catch(() => {});
5110
+ });
5111
+ // ↗(在 DSH 里打开完整对话):桌面=显式把会话开到宿主主区;web=开独立窗口。
5112
+ getEl("expandChatBtn").addEventListener("click", () => takeMainAreaOnHost());
5113
+ getEl("chatHostOpen").addEventListener("click", () => takeMainAreaOnHost());
5114
+ getEl("chatSessionBtn").addEventListener("click", (event) => {
5115
+ event.stopPropagation();
5116
+ toggleChatSessionMenu();
5117
+ });
5118
+ document.addEventListener("click", (event) => {
5119
+ const menu = getEl("chatSessionMenu");
5120
+ if (menu.hidden) return;
5121
+ if (menu.contains(event.target) || event.target.closest?.("#chatSessionBtn")) return;
5122
+ menu.hidden = true;
5123
+ });
5124
+ document.addEventListener("keydown", (event) => {
5125
+ if (event.key === "Escape") getEl("chatSessionMenu").hidden = true;
5126
+ });
5127
+ document.body.classList.add("desktop-host");
5128
+ refreshSessions().then(renderChatHeader).catch(() => renderChatHeader());
5129
+ }
5130
+
5131
+ /** 桌面:显式把会话开到宿主主区(优先当前会话,其次最近绑定,再不行 ensure 一个)。
5132
+ * 成功后宿主关闭工作台面板——这是唯一的「跳出工作台」路径。 */
5133
+ async function takeMainAreaOnHost() {
5134
+ const target = activeChatTarget();
5135
+ const current = currentChatSessionId();
5136
+ const target_ = target;
5137
+ const bound = target_ ? chatSessionsForTarget(target_) : [];
5138
+ const sessionId = current || bound[0]?.sessionId || "";
5139
+ return requestConversation({
5140
+ action: sessionId ? "open" : "ensure",
5141
+ sessionId,
5142
+ kind: target_?.kind,
5143
+ id: target_?.id,
5144
+ title: target_?.label,
5145
+ takeMain: true,
5146
+ prompt: sessionId
5147
+ ? undefined
5148
+ : target_
5149
+ ? `本对话绑定 ${target_.kind === "deck" ? `deck ${target_.id}(${target_.label})` : `风格设计 ${target_.label}`}。等我的指令,不要直接出图。`
5150
+ : undefined,
5151
+ });
5152
+ }
5153
+
4940
5154
  function copyText(text) {
4941
5155
  try {
4942
5156
  navigator.clipboard.writeText(text);
@@ -4946,13 +5160,13 @@ function copyText(text) {
4946
5160
  }
4947
5161
  }
4948
5162
 
4949
- /** 工作台 → 原生 Chat 的窗口:非 embed 用左栏自己的 iframe,embed 用宿主 DSH 壳。 */
5163
+ /** 工作台 → 原生 Chat 的窗口:非 embed 用左栏自己的 iframe;embed / 桌面用宿主 DSH 壳。 */
4950
5164
  function nativeChatWindow() {
4951
- if (!EMBED) {
5165
+ if (!EMBED && !DESKTOP_HOST) {
4952
5166
  const frame = getEl("nativeChatFrame");
4953
5167
  return frame && frame.contentWindow ? frame.contentWindow : null;
4954
5168
  }
4955
- return window.parent !== window ? window.parent : null; // embed:宿主 DSH 壳
5169
+ return window.parent !== window ? window.parent : null; // embed / 桌面:宿主 DSH 壳
4956
5170
  }
4957
5171
 
4958
5172
  function requestNativeChat(text) {
@@ -5007,7 +5221,12 @@ function requestConversation(payload) {
5007
5221
  cwd: STATE.data?.cwd || "",
5008
5222
  workspacePath: STATE.data?.workspacePath || "",
5009
5223
  sourceRoot: STATE.data?.active?.sourceRoot || "",
5224
+ // 0.2.16:宿主要按「deck 自己的项目目录」建会话工作区(桌面里 cwd 是 profile 目录,用不了)。
5225
+ sessionWorkspacePath: STATE.data?.active?.sessionWorkspacePath || "",
5010
5226
  ...payload,
5227
+ // F-202B:桌面 Dock 模式默认不抢主区(open:false);显式 takeMain → open:true 切宿主主区。
5228
+ // web 不传 open(undefined → 宿主按原语义 openSession)。
5229
+ open: DESKTOP_HOST ? payload.takeMain === true : undefined,
5011
5230
  }, "*");
5012
5231
  setTimeout(() => {
5013
5232
  if (settled) return;
@@ -5075,12 +5294,17 @@ async function ensureDeckConversation(deckId) {
5075
5294
  kind: "deck",
5076
5295
  id: deckId,
5077
5296
  title: label,
5297
+ // 桌面:点 deck 是进工作台,不是切出去聊天——后台 ensure,不切换宿主视图。
5298
+
5078
5299
  prompt: `本对话绑定 deck ${deckId}(${label})。后续所有修改都在这个 deck 上进行;先读 intent 与 script 概要,等我的指令,不要直接出图。`,
5079
5300
  });
5080
5301
  if (!result.ok) {
5081
5302
  getEl("chatBindChip").textContent = `会话打开失败:${result.reason || "未知原因"}`;
5303
+ flash(`会话打开失败:${result.reason || "未知原因"}`, "error");
5082
5304
  return result;
5083
5305
  }
5306
+ // 0.2.20:会话建好了但首条指令没发出去时,宿主会带 notice——别让它悄悄消失。
5307
+ if (result.notice) flash(result.notice);
5084
5308
  if (!binding || binding.sessionId !== result.sessionId) {
5085
5309
  await bindConversation({ sessionId: result.sessionId, kind: "deck", id: deckId, title: label });
5086
5310
  }
@@ -5127,6 +5351,12 @@ async function openBoundConversation(sessionId) {
5127
5351
  getEl("homeGreetSub").textContent = `没能切到会话:${result.reason || "未知原因"}`;
5128
5352
  return;
5129
5353
  }
5354
+ if (DESKTOP_HOST) {
5355
+ // 桌面:用户点会话就是要对话;宿主主区已切到该会话,收起工作台(回工作台时再对齐到绑定的 deck)。
5356
+ STATE.chatSessionId = sessionId;
5357
+ showDeck(binding?.kind === "deck" ? binding.id : null);
5358
+ return;
5359
+ }
5130
5360
  if (binding?.kind === "deck") {
5131
5361
  showDeck(binding.id);
5132
5362
  return;
@@ -5170,8 +5400,11 @@ function activeChatTarget() {
5170
5400
  return chatTargetForBinding({ kind: "deck", id: active.deckId });
5171
5401
  }
5172
5402
 
5173
- /** 当前 Chat 所在会话 id:优先读 iframe 里 shell 的 dsh.sessions.current(同源),退回本地记录。 */
5403
+ /** 当前 Chat 所在会话 id:优先读 iframe 里 shell 的 dsh.sessions.current(同源),退回本地记录。
5404
+ * F-202B:桌面宿主不读——隐藏 iframe 继承 dsh-app origin,localStorage 就是
5405
+ * 宿主 shell 的存储,读到的是「宿主当前会话」而不是本 deck 绑定会话(Dock 被它顶掉)。 */
5174
5406
  function currentChatSessionId() {
5407
+ if (DESKTOP_HOST) return STATE.chatSessionId || "";
5175
5408
  try {
5176
5409
  const win = getEl("nativeChatFrame")?.contentWindow;
5177
5410
  const raw = win?.localStorage?.getItem("dsh.sessions.current");
@@ -5284,6 +5517,7 @@ function renderChatSessionMenu(sessions, target) {
5284
5517
  STATE.chatSessionId = sessionId;
5285
5518
  await refreshSessions();
5286
5519
  renderChatHeader();
5520
+ // 桌面:会话已在宿主主区打开,把工作台面板让出来(web 版留在内嵌 iframe 里)。
5287
5521
  }));
5288
5522
  }
5289
5523
 
@@ -5476,19 +5710,33 @@ function renderChatHeader() {
5476
5710
  /** + 新建会话:建完立即绑定当前 deck / design(不再产生未绑定会话)。 */
5477
5711
  async function newChatForTarget() {
5478
5712
  const target = activeChatTarget();
5479
- if (!target) return;
5713
+ if (!target) {
5714
+ // 0.2.16:以前这里静默 return——点了 + 什么都不发生,用户只能猜。
5715
+ flash("当前没有可绑定的 deck / 风格:先打开一本 deck 再新建会话", "error");
5716
+ return;
5717
+ }
5480
5718
  const button = getEl("newChatBtn");
5481
5719
  button.disabled = true;
5482
5720
  try {
5483
5721
  const result = await requestConversation({ action: "ensure", kind: target.kind, id: target.id, title: target.label });
5484
5722
  if (!result.ok || !result.sessionId) {
5485
- getEl("chatSessionBtn").title = `新建会话失败:${result.reason || "未知原因"}`;
5723
+ const reason = result.reason || "未知原因";
5724
+ // F-202B:失败要用户可见——之前只写按钮 title,Dock 模式下看起来像「点了没反应」。
5725
+ // 0.2.16:再加一条常驻 toast,光靠 chip 文案还是容易被当成没反应。
5726
+ flash(`新建会话失败:${reason}`, "error");
5727
+ getEl("chatBindChip").textContent = "会话新建失败";
5728
+ getEl("chatBindChip").title = `新建会话失败:${reason};点这里重试`;
5729
+ getEl("chatSessionBtn").title = `新建会话失败:${reason}`;
5486
5730
  return;
5487
5731
  }
5488
5732
  STATE.chatSessionId = result.sessionId;
5733
+ if (result.notice) flash(result.notice);
5489
5734
  await bindConversation({ sessionId: result.sessionId, kind: target.kind, id: target.id, title: target.label });
5490
5735
  await refreshSessions();
5491
5736
  renderChatHeader();
5737
+ // 桌面:新建就是要点开聊——切到宿主主区的原生会话。
5738
+ } catch (error) {
5739
+ flash(`新建会话失败:${error?.message ?? error}`, "error");
5492
5740
  } finally {
5493
5741
  button.disabled = false;
5494
5742
  }
@@ -5761,7 +6009,7 @@ function backToManual(box) {
5761
6009
  getEl("updateBtn").addEventListener("click", checkPluginUpdate);
5762
6010
  getEl("repoBtn").addEventListener("click", openRepo);
5763
6011
  await refreshSessions();
5764
- getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : "对话在左侧 · DSH 原生";
6012
+ getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : DESKTOP_HOST ? "对话 · 原生(桌面 Dock)" : "对话在左侧 · DSH 原生";
5765
6013
  // F-69:只有 profile 开了 hideShellSidebar,宿主左侧栏才会默认收起;此时顶栏给一个切换入口。
5766
6014
  if (!EMBED && window.parent !== window && STATE.data?.hideShellSidebar === true) {
5767
6015
  const shellBtn = getEl("shellSidebarBtn");
@@ -5781,6 +6029,22 @@ function backToManual(box) {
5781
6029
  showHome();
5782
6030
  }
5783
6031
  setInterval(() => loadWorkbench(STATE.deckId), 5000);
6032
+
6033
+ /**
6034
+ * 0.2.21:boot 完成 → 把启动期间暂存的「打开某本 deck」执行掉,再向宿主报 ready。
6035
+ * 顺序很关键:先 booted=true 再执行,避免执行期间又来一条指令被丢进 pending 里没人取。
6036
+ */
6037
+ OPEN_BINDING.booted = true;
6038
+ if (OPEN_BINDING.pending) {
6039
+ const pending = OPEN_BINDING.pending;
6040
+ OPEN_BINDING.pending = null;
6041
+ applyOpenBinding(pending);
6042
+ }
6043
+ try {
6044
+ window.parent?.postMessage({ type: "dsh-deck-studio:workbench-ready" }, "*");
6045
+ } catch {
6046
+ /* 直接打开(无宿主)时没有 parent */
6047
+ }
5784
6048
  })();
5785
6049
  </script>
5786
6050
  </body>