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/client.js +494 -8
- package/index.js +81 -7
- package/lib/deck-templates.js +6 -1
- package/package.json +2 -2
- package/ui/workbench.css +26 -1
- package/ui/workbench.html +290 -26
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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>
|