dsh-plugin-deck-studio 0.2.13 → 0.2.14
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 +165 -5
- package/package.json +2 -2
- package/ui/workbench.css +9 -0
- package/ui/workbench.html +124 -5
package/client.js
CHANGED
|
@@ -29,6 +29,14 @@ window.__ModuleLoader__.load({
|
|
|
29
29
|
const WORKBENCH_KEY = "deck-studio-workbench";
|
|
30
30
|
const ROUTE_PREFIX = "/api/deck-studio.";
|
|
31
31
|
const POLL_MS = 5000;
|
|
32
|
+
/**
|
|
33
|
+
* F-202B(ADR-0027):桌面宿主的 shell 起源是自定义协议(如 dsh-app://),嵌 DSH 页面的
|
|
34
|
+
* 子帧拿不到 boot 注入、流式端点也不可达 —— 桌面里对话改用「原生会话 Dock」;
|
|
35
|
+
* web(http)宿主继续走工作台内嵌的 DSH iframe。
|
|
36
|
+
*/
|
|
37
|
+
const DESKTOP_SHELL = !/^(https?:)$/.test(location.protocol);
|
|
38
|
+
/** 桌面 Dock 渲染会话用的子弹槽名(shell.overlay 条目下的 session 作用域子槽)。 */
|
|
39
|
+
const CHAT_SLOT = `${WORKBENCH_KEY}.chat`;
|
|
32
40
|
|
|
33
41
|
/** 路由白名单:getJson 只放行这一族地址。 */
|
|
34
42
|
async function getJson(path) {
|
|
@@ -274,9 +282,21 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
274
282
|
* 打开先到主页(#/home/decks),点 deck 后是左中右三栏:左原生 Chat / 中间整页图片 / 右资料库。
|
|
275
283
|
* 对话仍走 DSH 原生 Chat(工作台左栏嵌 `/?decklineChat=1`),权限、附件、模型、思考过程、气泡都是原生的。
|
|
276
284
|
*/
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 桌面 Dock(F-202B / ADR-0027 升级):桌面里不再嵌 DSH 页面,用宿主 `shell.overlay` +
|
|
288
|
+
* SessionProvider + `sessions.retain` 把**原生会话**渲染到工作台对话列的位置
|
|
289
|
+
* (multichat / html-deck-studio 同款做法,注册形状已在该宿主验证)。
|
|
290
|
+
* 工作台(iframe)把当前绑定会话经 `dsh-deck-studio:chat-session` 报上来;
|
|
291
|
+
* Dock 测量工作台 iframe 里 `#chatDockArea` 的矩形后固定覆盖其上。
|
|
292
|
+
*/
|
|
293
|
+
/** 客户端插件体:样式 + 槽位注册。 */
|
|
294
|
+
function apply(ctx) {
|
|
295
|
+
injectStylesheet();
|
|
277
296
|
function WorkbenchFrame() {
|
|
278
297
|
return h("iframe", {
|
|
279
298
|
id: "dsh-deck-workbench-frame",
|
|
299
|
+
ref: (node) => setWorkbenchFrame(node),
|
|
280
300
|
title: "Deckline 工作台",
|
|
281
301
|
src: "/api/deck-studio.app#/home/decks",
|
|
282
302
|
style: { width: "100%", height: "100%", border: 0, display: "block", background: "#faf8f2" },
|
|
@@ -286,9 +306,94 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
286
306
|
});
|
|
287
307
|
}
|
|
288
308
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
309
|
+
const dockState = { sessionId: "", listeners: new Set() };
|
|
310
|
+
function publishDockSession(sessionId) {
|
|
311
|
+
const next = typeof sessionId === "string" ? sessionId : "";
|
|
312
|
+
if (dockState.sessionId === next) return;
|
|
313
|
+
dockState.sessionId = next;
|
|
314
|
+
for (const listener of dockState.listeners) listener();
|
|
315
|
+
}
|
|
316
|
+
function subscribeDockSession(listener) {
|
|
317
|
+
dockState.listeners.add(listener);
|
|
318
|
+
return () => dockState.listeners.delete(listener);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 工作台 iframe 元素引用(React ref);Dock 靠它测矩形 / 报「Dock 已就绪」。 */
|
|
322
|
+
const frameState = { el: null, listeners: new Set() };
|
|
323
|
+
function setWorkbenchFrame(node) {
|
|
324
|
+
const next = node ?? null;
|
|
325
|
+
if (frameState.el === next) return;
|
|
326
|
+
frameState.el = next;
|
|
327
|
+
for (const listener of frameState.listeners) listener();
|
|
328
|
+
}
|
|
329
|
+
function subscribeWorkbenchFrame(listener) {
|
|
330
|
+
frameState.listeners.add(listener);
|
|
331
|
+
return () => frameState.listeners.delete(listener);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** 会话内容视图:复用宿主 `conversation.session` 槽(不是复制品)。 */
|
|
335
|
+
function DeckChatView(props) {
|
|
336
|
+
return props.renderSlot("conversation.session", { view: "chat" });
|
|
337
|
+
}
|
|
338
|
+
function DeckChatBody({ useSession, useConversation, renderFactorySlot }) {
|
|
339
|
+
const session = useSession((value) => value);
|
|
340
|
+
const active = useConversation((value) => value).activeTargets;
|
|
341
|
+
const phase = (!session.blank || session.running || active.size > 0) ? "active" : "hero";
|
|
342
|
+
return renderFactorySlot("conversation.content", { variant: "embedded", phase, hero: phase === "hero" }, { slots: { views: DeckChatView } });
|
|
343
|
+
}
|
|
344
|
+
/** Dock:测量工作台对话占位矩形;用 retain 的会话引用喂 SessionProvider。 */
|
|
345
|
+
function DeckChatDock({ SessionProvider, renderSlot }) {
|
|
346
|
+
const sessionId = React.useSyncExternalStore(subscribeDockSession, () => dockState.sessionId);
|
|
347
|
+
// 主区面板卸载/重建时 iframe 元素会换,Dock 必须跟着重算位置,否则切走再切回来就不见了。
|
|
348
|
+
const frameEl = React.useSyncExternalStore(subscribeWorkbenchFrame, () => frameState.el);
|
|
349
|
+
const [rect, setRect] = React.useState(null);
|
|
350
|
+
const [reference, setReference] = React.useState(null);
|
|
351
|
+
React.useEffect(() => {
|
|
352
|
+
const measure = () => {
|
|
353
|
+
let next = null;
|
|
354
|
+
try {
|
|
355
|
+
const outer = frameEl?.getBoundingClientRect?.();
|
|
356
|
+
// 只盖住对话框的内容区(#chatDockArea),把上面的绑定/+/会话菜单那一行留出来。
|
|
357
|
+
const inner = frameEl?.contentDocument?.querySelector("#chatDockArea")?.getBoundingClientRect?.();
|
|
358
|
+
if (outer && inner && outer.width > 0 && inner.width > 4 && inner.height > 4) {
|
|
359
|
+
next = {
|
|
360
|
+
left: Math.round(outer.left + inner.left),
|
|
361
|
+
top: Math.round(outer.top + inner.top),
|
|
362
|
+
width: Math.round(inner.width),
|
|
363
|
+
height: Math.round(inner.height),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
next = null;
|
|
368
|
+
}
|
|
369
|
+
setRect((prev) => (prev && next && prev.left === next.left && prev.top === next.top && prev.width === next.width && prev.height === next.height) ? prev : next);
|
|
370
|
+
};
|
|
371
|
+
measure();
|
|
372
|
+
if (!frameEl) { setRect(null); return undefined; }
|
|
373
|
+
const timer = setInterval(measure, 800);
|
|
374
|
+
window.addEventListener("resize", measure);
|
|
375
|
+
return () => { clearInterval(timer); window.removeEventListener("resize", measure); };
|
|
376
|
+
}, [frameEl]);
|
|
377
|
+
// 引用放 state(不是 ref):StrictMode 的 effect 重放与 ref 释放会让提交渲染时 ref 为空。
|
|
378
|
+
React.useEffect(() => {
|
|
379
|
+
const sessions = ctx.sessions;
|
|
380
|
+
if (!sessionId || typeof sessions?.retain !== "function") { setReference(null); return undefined; }
|
|
381
|
+
let held = null;
|
|
382
|
+
try { held = sessions.retain(sessionId, { source: WORKBENCH_KEY }); } catch (error) { ctx.logger?.warn?.(`deck-studio: retain failed: ${error?.message ?? error}`); }
|
|
383
|
+
setReference(held ?? null);
|
|
384
|
+
return () => { try { held?.release?.(); } catch { /* 已释放 */ } };
|
|
385
|
+
}, [sessionId]);
|
|
386
|
+
// Dock 已在渲染:告诉工作台 iframe 收起降级卡(#chatHostCard)。
|
|
387
|
+
React.useEffect(() => {
|
|
388
|
+
if (!rect || !reference || !frameEl?.contentWindow) return;
|
|
389
|
+
try { frameEl.contentWindow.postMessage({ type: "dsh-deck-studio:chat-dock" }, "*"); } catch { /* 不可达时降级卡留着 */ }
|
|
390
|
+
}, [rect, reference]);
|
|
391
|
+
if (!SessionProvider || !renderSlot || !reference || !rect) return null;
|
|
392
|
+
return h("div", {
|
|
393
|
+
"data-deck-studio-chat-dock": "",
|
|
394
|
+
style: { position: "fixed", left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px`, zIndex: 40, display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden", background: "var(--paper,#faf8f2)", borderRight: "1px solid var(--line,#e8e2d6)", pointerEvents: "auto" },
|
|
395
|
+
}, h(SessionProvider, { session: reference }, renderSlot(CHAT_SLOT, {})));
|
|
396
|
+
}
|
|
292
397
|
// 工作台左栏会把 DSH 原生 Chat 以 `/?decklineChat=1` 嵌进来(同源 iframe)。
|
|
293
398
|
// 那个嵌套的 DSH 实例不再自动打开工作台,否则会层层嵌套(F-68)。
|
|
294
399
|
const chatEmbed = new URLSearchParams(window.location.search).get("decklineChat") === "1";
|
|
@@ -431,7 +536,11 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
431
536
|
// 不把插件安装目录或 DSH 私有数据目录登记成用户工作区。
|
|
432
537
|
const path = normalize(cwd);
|
|
433
538
|
const internal = /(^|\/)node_modules(\/|$)|(^|\/)\.dsh(\/|$)/.test(path);
|
|
434
|
-
|
|
539
|
+
// F-202B:桌面宿主里 shell 的 process.cwd 是 App 包目录(/Applications/DeepSeek Harness.app)
|
|
540
|
+
// ——把它登记成用户工作区是错的(web 宿主的 cwd 才是启动目录)。这类系统路径视为不可信,
|
|
541
|
+
// 落到 sourceRoot / 默认工作区兜底,而不是新建一个垃圾工作区。
|
|
542
|
+
const systemPath = path === "/" || path.startsWith("/Applications/") || /^(\/System(\/|$)|\/Library(\/|$)|\/private\/var|\/usr\/|\/opt\/|\/Volumes\/)/.test(path);
|
|
543
|
+
if (path && !internal && !systemPath) return register(cwd);
|
|
435
544
|
}
|
|
436
545
|
const sourceRoot = String(data.sourceRoot ?? "").trim();
|
|
437
546
|
const sourceWorkspace = sourceRoot ? containing(sourceRoot) : null;
|
|
@@ -486,7 +595,25 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
486
595
|
let sessionId = String(data.sessionId ?? "").trim();
|
|
487
596
|
if (data.action === "ensure") sessionId = await ensureConversationSession(data);
|
|
488
597
|
if (!sessionId) throw new Error("没有目标会话");
|
|
489
|
-
|
|
598
|
+
/**
|
|
599
|
+
* F-202B(ADR-0027):`open !== false` = 把会话切到宿主主区(web 内嵌 iframe 的原语义,
|
|
600
|
+
* 桌面「在主区打开」的显式入口);`open:false` = 桌面 Dock 模式——会话已在工作台
|
|
601
|
+
* 左栏原生渲染,只把会话准备好并回 id,不抢宿主主区。会话目录未就绪时短重试
|
|
602
|
+
* (`sessions.retain: unknown session...` 是自愈错误,不是会话丢失)。
|
|
603
|
+
*/
|
|
604
|
+
if ((data.action === "open" || data.action === "ensure") && data.open !== false) {
|
|
605
|
+
const delays = [0, 300, 700, 1500, 2500];
|
|
606
|
+
let lastError = null;
|
|
607
|
+
for (const delay of delays) {
|
|
608
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
609
|
+
try { await uiWorkspace.openSession(sessionId); lastError = null; break; } catch (error) {
|
|
610
|
+
lastError = error;
|
|
611
|
+
if (!/sessions\.retain|unknown session/i.test(String(error?.message ?? error))) throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (lastError) throw lastError;
|
|
615
|
+
if (data.takeMain === true && DESKTOP_SHELL) closeWorkbench();
|
|
616
|
+
}
|
|
490
617
|
if (data.prompt && data.action === "ensure" && sessionId !== String(data.sessionId ?? "")) {
|
|
491
618
|
await sendToSession(sessionId, String(data.prompt));
|
|
492
619
|
}
|
|
@@ -500,6 +627,39 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
|
|
|
500
627
|
return () => window.removeEventListener("message", onMessage);
|
|
501
628
|
}, "deck-studio: conversation bridge");
|
|
502
629
|
|
|
630
|
+
/* 桌面 Dock 的会话源:工作台(iframe)周期性把当前绑定会话报上来。 */
|
|
631
|
+
ctx.effect(() => {
|
|
632
|
+
const onMessage = (event) => {
|
|
633
|
+
const data = event?.data ?? {};
|
|
634
|
+
if (data?.type !== "dsh-deck-studio:chat-session" || typeof data.sessionId !== "string") return;
|
|
635
|
+
if (event.source !== (frameState.el?.contentWindow ?? null)) return;
|
|
636
|
+
publishDockSession(data.sessionId);
|
|
637
|
+
};
|
|
638
|
+
window.addEventListener("message", onMessage);
|
|
639
|
+
return () => window.removeEventListener("message", onMessage);
|
|
640
|
+
}, "deck-studio: chat-session bridge");
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* F-202B:桌面「原生会话 Dock」——shell.overlay 容器 + session 作用域内容槽
|
|
644
|
+
* (multichat / html-deck-studio 同款注册形状,SessionProvider 由宿主注入)。
|
|
645
|
+
* web(http 起源)不用 Dock:工作台 iframe 里的 DSH 对话本来就是好的。
|
|
646
|
+
*/
|
|
647
|
+
if (DESKTOP_SHELL) {
|
|
648
|
+
ctx.effect(
|
|
649
|
+
() => ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
650
|
+
name: "shell.overlay",
|
|
651
|
+
id: `${WORKBENCH_KEY}.chat`,
|
|
652
|
+
order: 58,
|
|
653
|
+
children: { [CHAT_SLOT]: { kind: "single", scope: "session" } },
|
|
654
|
+
}, DeckChatDock)),
|
|
655
|
+
"deck-studio: chat dock",
|
|
656
|
+
);
|
|
657
|
+
ctx.effect(
|
|
658
|
+
() => ctx.slots.inject(CHAT_SLOT, () => ctx.slots.register({ name: CHAT_SLOT }, DeckChatBody)),
|
|
659
|
+
"deck-studio: chat dock body",
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
503
663
|
if (!chatEmbed) {
|
|
504
664
|
/* 工作台顶栏「☰ DSH」→ 宿主:临时展开 / 收起左侧栏(需要看会话、插件、设置时用)。 */
|
|
505
665
|
ctx.effect(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-deck-studio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "Deckline:DSH 的 image deck / PPT 工作台插件——原生 Chat + 整页图片 deck 工作区 + 设计风格库;用对话做 PPT、按页出图、版本可追踪、导出 PDF/PPTX。",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"homepage": "https://github.com/yuna78/dsh-deck-studio#readme",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"verify": "node verify/smoke.mjs",
|
|
48
|
-
"prepublishOnly": "node verify/smoke.mjs",
|
|
48
|
+
"prepublishOnly": "node -e \"const fs=require('fs');const{execSync}=require('child_process');if(fs.existsSync('verify/smoke.mjs')){execSync('node verify/smoke.mjs',{stdio:'inherit'})}else{console.log('[prepublishOnly] verify/ 不在发布目录(公开包白名单不含),跳过;发布前请在开发仓跑 npm run smoke')}\"",
|
|
49
49
|
"prepack": "node scripts/prepack-assets.mjs"
|
|
50
50
|
},
|
|
51
51
|
"dsh": {
|
package/ui/workbench.css
CHANGED
|
@@ -479,6 +479,15 @@ body.has-chat .layout{grid-template-columns:var(--nav-w,430px) 5px minmax(0,1fr)
|
|
|
479
479
|
.chat-session-item span{font-size:9.5px;color:#8a9599}
|
|
480
480
|
.chat-session-empty{margin:6px 8px 8px;font-size:10.5px;color:#8a9599;line-height:1.6}
|
|
481
481
|
#nativeChatFrame{flex:1;min-height:0;width:100%;border:0;background:var(--paper)}
|
|
482
|
+
/* F-202B:桌面宿主(dsh-app://app)不嵌 DSH 页面——工作台对话列留一个 Dock 占位,
|
|
483
|
+
* 宿主用 shell.overlay 把原生会话精确盖在这里;原生 Dock 不可达时回退 #chatHostCard。 */
|
|
484
|
+
#chatDockArea{flex:1;min-height:0;background:var(--paper)}
|
|
485
|
+
#chatDockArea[hidden]{display:none}
|
|
486
|
+
#chatDockArea:empty::before{content:"还没有绑定会话——点上方 + 新建";margin:44px auto;display:block;width:80%;text-align:center;color:#8a9599;font-size:11px}
|
|
487
|
+
#chatHostCard{flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:26px 20px;overflow:auto}
|
|
488
|
+
#chatHostCard[hidden]{display:none}
|
|
489
|
+
#chatHostCard .chat-host-hint{margin:0;font-size:11px;line-height:1.7;color:var(--ink);opacity:.78;text-align:center;max-width:30em}
|
|
490
|
+
#chatHostCard .btn{min-width:180px;text-align:center}
|
|
482
491
|
|
|
483
492
|
/* 叙事结构概览(脚本页顶部,从 script 派生) */
|
|
484
493
|
.story-overview{margin:10px 0 18px;border:1px solid var(--line);border-radius:7px;background:#f8f6f0;padding:10px 14px}
|
package/ui/workbench.html
CHANGED
|
@@ -286,6 +286,11 @@
|
|
|
286
286
|
</div>
|
|
287
287
|
<div class="chat-session-menu" id="chatSessionMenu" hidden></div>
|
|
288
288
|
</div>
|
|
289
|
+
<div id="chatDockArea" hidden title="这里由宿主渲染原生会话(桌面 Dock)"></div>
|
|
290
|
+
<div id="chatHostCard" hidden>
|
|
291
|
+
<p class="chat-host-hint">桌面 App 里内嵌 DSH 页面无法启动(子帧拿不到 boot 注入)。宿主原生会话 Dock 没有就绪——对话将<b>在宿主主区</b>打开,点 ☰ 或左侧栏 Deckline 行切回工作台。</p>
|
|
292
|
+
<button type="button" class="btn alt" id="chatHostOpen">在宿主主区打开对话 →</button>
|
|
293
|
+
</div>
|
|
289
294
|
<iframe id="nativeChatFrame" title="DSH 原生对话"></iframe>
|
|
290
295
|
</aside>
|
|
291
296
|
<div class="splitter" data-split="nav" title="拖动调整对话栏宽度" hidden></div>
|
|
@@ -583,6 +588,14 @@ let SETTINGS_FORM_DIRTY = false;
|
|
|
583
588
|
|
|
584
589
|
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
590
|
const EMBED = new URLSearchParams(location.search).get("embed") === "1";
|
|
591
|
+
/**
|
|
592
|
+
* F-202B:桌面宿主(自定义协议 `dsh-app://app`)的 DSH 子帧拿不到 boot 注入(`__DSH_BOOT_READY__`
|
|
593
|
+
* 只注入主帧),流式端点也在子帧 404 —— `/?decklineChat=1` 嵌套实例永远停在「Loading plugins…」。
|
|
594
|
+
* 桌面上对话改走宿主原生(client.js 的 conversation 桥 → 宿主 uiWorkspace.openSession,会话开在宿主主区);
|
|
595
|
+
* web 版(http 起源,如 13090 / 3080)保持 iframe 内嵌不动。
|
|
596
|
+
*/
|
|
597
|
+
const DESKTOP_HOST = !/^(https?:)$/.test(location.protocol)
|
|
598
|
+
|| new URLSearchParams(location.search).get("chatHost") === "panel";
|
|
586
599
|
|
|
587
600
|
const esc = (v) => String(v ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
588
601
|
const getEl = (id) => document.getElementById(id);
|
|
@@ -4898,6 +4911,7 @@ function decorateNativeChat(doc) {
|
|
|
4898
4911
|
|
|
4899
4912
|
function setupNativeChat() {
|
|
4900
4913
|
if (EMBED) return; // embed 模式下宿主 shell 本身就有原生 Chat
|
|
4914
|
+
if (DESKTOP_HOST) return setupHostChatColumn(); // F-202B:桌面不嵌 DSH 页面
|
|
4901
4915
|
const column = getEl("chatCol");
|
|
4902
4916
|
const frame = getEl("nativeChatFrame");
|
|
4903
4917
|
const splitter = document.querySelector('.splitter[data-split="nav"]');
|
|
@@ -4937,6 +4951,92 @@ function setupNativeChat() {
|
|
|
4937
4951
|
});
|
|
4938
4952
|
}
|
|
4939
4953
|
|
|
4954
|
+
/**
|
|
4955
|
+
* 桌面(dsh-app://app)的对话列:不嵌 DSH 页面,把头部保留成「绑定 + 会话切换」,
|
|
4956
|
+
* 动作经宿主桥(client.js)在宿主主区打开原生会话;打开成功后关闭工作台面板。
|
|
4957
|
+
*/
|
|
4958
|
+
function setupHostChatColumn() {
|
|
4959
|
+
const column = getEl("chatCol");
|
|
4960
|
+
const frame = getEl("nativeChatFrame");
|
|
4961
|
+
const splitter = document.querySelector('.splitter[data-split="nav"]');
|
|
4962
|
+
if (!column || !frame) return;
|
|
4963
|
+
column.hidden = false;
|
|
4964
|
+
if (splitter) splitter.hidden = false;
|
|
4965
|
+
document.body.classList.add("has-chat");
|
|
4966
|
+
frame.hidden = true;
|
|
4967
|
+
getEl("chatDockArea").hidden = false;
|
|
4968
|
+
STATE.dockActive = false;
|
|
4969
|
+
// 降级卡:宿主原生 Dock 没接上(少见;比如 shell.overlay 契约缺失)时,退回「在主区打开」。
|
|
4970
|
+
const fallbackTimer = setTimeout(() => {
|
|
4971
|
+
if (!STATE.dockActive) getEl("chatHostCard").hidden = false;
|
|
4972
|
+
}, 6000);
|
|
4973
|
+
// 心跳把当前绑定会话报给宿主:Dock 据此用原生会话盖住 #chatDockArea。
|
|
4974
|
+
let lastPublished = "§init§";
|
|
4975
|
+
const publishChatSession = () => {
|
|
4976
|
+
const sessionId = currentChatSessionId();
|
|
4977
|
+
if (sessionId === lastPublished) return;
|
|
4978
|
+
lastPublished = sessionId;
|
|
4979
|
+
try { window.parent?.postMessage({ type: "dsh-deck-studio:chat-session", sessionId }, "*"); } catch { /* 宿主桥不可用 */ }
|
|
4980
|
+
};
|
|
4981
|
+
setInterval(publishChatSession, 1200);
|
|
4982
|
+
try { window.parent?.postMessage({ type: "dsh-deck-studio:chat-session", sessionId: lastPublished = currentChatSessionId() }, "*"); } catch { /* 同上 */ }
|
|
4983
|
+
window.addEventListener("message", (event) => {
|
|
4984
|
+
if (event.source !== window.parent) return;
|
|
4985
|
+
if (event.data?.type !== "dsh-deck-studio:chat-dock") return;
|
|
4986
|
+
STATE.dockActive = true;
|
|
4987
|
+
getEl("chatHostCard").hidden = true;
|
|
4988
|
+
clearInterval(fallbackTimer);
|
|
4989
|
+
});
|
|
4990
|
+
getEl("chatBindChip").addEventListener("click", () => {
|
|
4991
|
+
if (!getEl("chatBindChip").classList.contains("unbound")) return;
|
|
4992
|
+
bindCurrentChatToTarget().catch(() => {});
|
|
4993
|
+
});
|
|
4994
|
+
getEl("newChatBtn").addEventListener("click", () => {
|
|
4995
|
+
newChatForTarget().catch(() => {});
|
|
4996
|
+
});
|
|
4997
|
+
// ↗(在 DSH 里打开完整对话):桌面=显式把会话开到宿主主区;web=开独立窗口。
|
|
4998
|
+
getEl("expandChatBtn").addEventListener("click", () => takeMainAreaOnHost());
|
|
4999
|
+
getEl("chatHostOpen").addEventListener("click", () => takeMainAreaOnHost());
|
|
5000
|
+
getEl("chatSessionBtn").addEventListener("click", (event) => {
|
|
5001
|
+
event.stopPropagation();
|
|
5002
|
+
toggleChatSessionMenu();
|
|
5003
|
+
});
|
|
5004
|
+
document.addEventListener("click", (event) => {
|
|
5005
|
+
const menu = getEl("chatSessionMenu");
|
|
5006
|
+
if (menu.hidden) return;
|
|
5007
|
+
if (menu.contains(event.target) || event.target.closest?.("#chatSessionBtn")) return;
|
|
5008
|
+
menu.hidden = true;
|
|
5009
|
+
});
|
|
5010
|
+
document.addEventListener("keydown", (event) => {
|
|
5011
|
+
if (event.key === "Escape") getEl("chatSessionMenu").hidden = true;
|
|
5012
|
+
});
|
|
5013
|
+
document.body.classList.add("desktop-host");
|
|
5014
|
+
refreshSessions().then(renderChatHeader).catch(() => renderChatHeader());
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
/** 桌面:显式把会话开到宿主主区(优先当前会话,其次最近绑定,再不行 ensure 一个)。
|
|
5018
|
+
* 成功后宿主关闭工作台面板——这是唯一的「跳出工作台」路径。 */
|
|
5019
|
+
async function takeMainAreaOnHost() {
|
|
5020
|
+
const target = activeChatTarget();
|
|
5021
|
+
const current = currentChatSessionId();
|
|
5022
|
+
const target_ = target;
|
|
5023
|
+
const bound = target_ ? chatSessionsForTarget(target_) : [];
|
|
5024
|
+
const sessionId = current || bound[0]?.sessionId || "";
|
|
5025
|
+
return requestConversation({
|
|
5026
|
+
action: sessionId ? "open" : "ensure",
|
|
5027
|
+
sessionId,
|
|
5028
|
+
kind: target_?.kind,
|
|
5029
|
+
id: target_?.id,
|
|
5030
|
+
title: target_?.label,
|
|
5031
|
+
takeMain: true,
|
|
5032
|
+
prompt: sessionId
|
|
5033
|
+
? undefined
|
|
5034
|
+
: target_
|
|
5035
|
+
? `本对话绑定 ${target_.kind === "deck" ? `deck ${target_.id}(${target_.label})` : `风格设计 ${target_.label}`}。等我的指令,不要直接出图。`
|
|
5036
|
+
: undefined,
|
|
5037
|
+
});
|
|
5038
|
+
}
|
|
5039
|
+
|
|
4940
5040
|
function copyText(text) {
|
|
4941
5041
|
try {
|
|
4942
5042
|
navigator.clipboard.writeText(text);
|
|
@@ -4946,13 +5046,13 @@ function copyText(text) {
|
|
|
4946
5046
|
}
|
|
4947
5047
|
}
|
|
4948
5048
|
|
|
4949
|
-
/** 工作台 → 原生 Chat 的窗口:非 embed 用左栏自己的 iframe
|
|
5049
|
+
/** 工作台 → 原生 Chat 的窗口:非 embed 用左栏自己的 iframe;embed / 桌面用宿主 DSH 壳。 */
|
|
4950
5050
|
function nativeChatWindow() {
|
|
4951
|
-
if (!EMBED) {
|
|
5051
|
+
if (!EMBED && !DESKTOP_HOST) {
|
|
4952
5052
|
const frame = getEl("nativeChatFrame");
|
|
4953
5053
|
return frame && frame.contentWindow ? frame.contentWindow : null;
|
|
4954
5054
|
}
|
|
4955
|
-
return window.parent !== window ? window.parent : null; // embed
|
|
5055
|
+
return window.parent !== window ? window.parent : null; // embed / 桌面:宿主 DSH 壳
|
|
4956
5056
|
}
|
|
4957
5057
|
|
|
4958
5058
|
function requestNativeChat(text) {
|
|
@@ -5008,6 +5108,9 @@ function requestConversation(payload) {
|
|
|
5008
5108
|
workspacePath: STATE.data?.workspacePath || "",
|
|
5009
5109
|
sourceRoot: STATE.data?.active?.sourceRoot || "",
|
|
5010
5110
|
...payload,
|
|
5111
|
+
// F-202B:桌面 Dock 模式默认不抢主区(open:false);显式 takeMain → open:true 切宿主主区。
|
|
5112
|
+
// web 不传 open(undefined → 宿主按原语义 openSession)。
|
|
5113
|
+
open: DESKTOP_HOST ? payload.takeMain === true : undefined,
|
|
5011
5114
|
}, "*");
|
|
5012
5115
|
setTimeout(() => {
|
|
5013
5116
|
if (settled) return;
|
|
@@ -5075,6 +5178,8 @@ async function ensureDeckConversation(deckId) {
|
|
|
5075
5178
|
kind: "deck",
|
|
5076
5179
|
id: deckId,
|
|
5077
5180
|
title: label,
|
|
5181
|
+
// 桌面:点 deck 是进工作台,不是切出去聊天——后台 ensure,不切换宿主视图。
|
|
5182
|
+
|
|
5078
5183
|
prompt: `本对话绑定 deck ${deckId}(${label})。后续所有修改都在这个 deck 上进行;先读 intent 与 script 概要,等我的指令,不要直接出图。`,
|
|
5079
5184
|
});
|
|
5080
5185
|
if (!result.ok) {
|
|
@@ -5127,6 +5232,12 @@ async function openBoundConversation(sessionId) {
|
|
|
5127
5232
|
getEl("homeGreetSub").textContent = `没能切到会话:${result.reason || "未知原因"}`;
|
|
5128
5233
|
return;
|
|
5129
5234
|
}
|
|
5235
|
+
if (DESKTOP_HOST) {
|
|
5236
|
+
// 桌面:用户点会话就是要对话;宿主主区已切到该会话,收起工作台(回工作台时再对齐到绑定的 deck)。
|
|
5237
|
+
STATE.chatSessionId = sessionId;
|
|
5238
|
+
showDeck(binding?.kind === "deck" ? binding.id : null);
|
|
5239
|
+
return;
|
|
5240
|
+
}
|
|
5130
5241
|
if (binding?.kind === "deck") {
|
|
5131
5242
|
showDeck(binding.id);
|
|
5132
5243
|
return;
|
|
@@ -5170,8 +5281,11 @@ function activeChatTarget() {
|
|
|
5170
5281
|
return chatTargetForBinding({ kind: "deck", id: active.deckId });
|
|
5171
5282
|
}
|
|
5172
5283
|
|
|
5173
|
-
/** 当前 Chat 所在会话 id:优先读 iframe 里 shell 的 dsh.sessions.current(同源),退回本地记录。
|
|
5284
|
+
/** 当前 Chat 所在会话 id:优先读 iframe 里 shell 的 dsh.sessions.current(同源),退回本地记录。
|
|
5285
|
+
* F-202B:桌面宿主不读——隐藏 iframe 继承 dsh-app origin,localStorage 就是
|
|
5286
|
+
* 宿主 shell 的存储,读到的是「宿主当前会话」而不是本 deck 绑定会话(Dock 被它顶掉)。 */
|
|
5174
5287
|
function currentChatSessionId() {
|
|
5288
|
+
if (DESKTOP_HOST) return STATE.chatSessionId || "";
|
|
5175
5289
|
try {
|
|
5176
5290
|
const win = getEl("nativeChatFrame")?.contentWindow;
|
|
5177
5291
|
const raw = win?.localStorage?.getItem("dsh.sessions.current");
|
|
@@ -5284,6 +5398,7 @@ function renderChatSessionMenu(sessions, target) {
|
|
|
5284
5398
|
STATE.chatSessionId = sessionId;
|
|
5285
5399
|
await refreshSessions();
|
|
5286
5400
|
renderChatHeader();
|
|
5401
|
+
// 桌面:会话已在宿主主区打开,把工作台面板让出来(web 版留在内嵌 iframe 里)。
|
|
5287
5402
|
}));
|
|
5288
5403
|
}
|
|
5289
5404
|
|
|
@@ -5482,6 +5597,9 @@ async function newChatForTarget() {
|
|
|
5482
5597
|
try {
|
|
5483
5598
|
const result = await requestConversation({ action: "ensure", kind: target.kind, id: target.id, title: target.label });
|
|
5484
5599
|
if (!result.ok || !result.sessionId) {
|
|
5600
|
+
// F-202B:失败要用户可见——之前只写按钮 title,Dock 模式下看起来像「点了没反应」。
|
|
5601
|
+
getEl("chatBindChip").textContent = "会话新建失败";
|
|
5602
|
+
getEl("chatBindChip").title = `新建会话失败:${result.reason || "未知原因"};点这里重试`;
|
|
5485
5603
|
getEl("chatSessionBtn").title = `新建会话失败:${result.reason || "未知原因"}`;
|
|
5486
5604
|
return;
|
|
5487
5605
|
}
|
|
@@ -5489,6 +5607,7 @@ async function newChatForTarget() {
|
|
|
5489
5607
|
await bindConversation({ sessionId: result.sessionId, kind: target.kind, id: target.id, title: target.label });
|
|
5490
5608
|
await refreshSessions();
|
|
5491
5609
|
renderChatHeader();
|
|
5610
|
+
// 桌面:新建就是要点开聊——切到宿主主区的原生会话。
|
|
5492
5611
|
} finally {
|
|
5493
5612
|
button.disabled = false;
|
|
5494
5613
|
}
|
|
@@ -5761,7 +5880,7 @@ function backToManual(box) {
|
|
|
5761
5880
|
getEl("updateBtn").addEventListener("click", checkPluginUpdate);
|
|
5762
5881
|
getEl("repoBtn").addEventListener("click", openRepo);
|
|
5763
5882
|
await refreshSessions();
|
|
5764
|
-
getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : "对话在左侧 · DSH 原生";
|
|
5883
|
+
getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : DESKTOP_HOST ? "对话 · 原生(桌面 Dock)" : "对话在左侧 · DSH 原生";
|
|
5765
5884
|
// F-69:只有 profile 开了 hideShellSidebar,宿主左侧栏才会默认收起;此时顶栏给一个切换入口。
|
|
5766
5885
|
if (!EMBED && window.parent !== window && STATE.data?.hideShellSidebar === true) {
|
|
5767
5886
|
const shellBtn = getEl("shellSidebarBtn");
|