dsh-plugin-deck-studio 0.2.12 → 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 CHANGED
@@ -5,8 +5,8 @@
5
5
  * 数据全部由宿主半聚合好(浏览器半只做展示,不重算业务口径)。
6
6
  *
7
7
  * 形态:主区「Deck 工作台」面板(iframe 复用 /api/deck-studio.app?embed=1)。
8
- * 启动后自动打开(零点击进入),左侧 foote 只保留一个「Deckline」入口用于重新打开;
9
- * 不再注册右侧 dockkit 观测标签页(F-63:入口太多且丑)。
8
+ * 启动后自动打开(零点击进入),侧栏只保留一个带图标的「Deckline」面板入口(与其它插件同形态);
9
+ * 不再注册右侧 dockkit 观测标签页(F-63:入口太多且丑),也不再占用侧栏脚部按钮。
10
10
  * 宿主没有 layout 主区面板契约时降级为 main 面板 + sidebar.panellist。
11
11
  *
12
12
  * Chatbox 说明(ADR-0006 D1):对话入口就是 DSH 原生 Chat——选择
@@ -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) {
@@ -235,9 +243,34 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
235
243
  );
236
244
  }
237
245
 
238
- /** 侧栏 glyph(降级 panellist / footer 用)。 */
239
- function DeckGlyph() {
240
- return h("span", { style: { fontSize: 13 } }, "▦");
246
+ /** 侧栏图标(panellist 行用):叠卡片 + 星芒,单色 currentColor,跟随主题深浅色。 */
247
+ function DeckGlyph({ size }) {
248
+ const edge = size || 16;
249
+ return h("svg", {
250
+ width: edge,
251
+ height: edge,
252
+ viewBox: "0 0 16 16",
253
+ fill: "none",
254
+ "aria-hidden": "true",
255
+ style: { display: "block" },
256
+ },
257
+ h("rect", {
258
+ x: 4.8, y: 2.4, width: 8.2, height: 6.2, rx: 1.5,
259
+ stroke: "currentColor", strokeWidth: 1.2, opacity: 0.55,
260
+ }),
261
+ h("rect", {
262
+ x: 2.4, y: 5.2, width: 9.2, height: 6.8, rx: 1.6,
263
+ stroke: "currentColor", strokeWidth: 1.35,
264
+ }),
265
+ h("path", {
266
+ d: "M4.6 7.6h4.6M4.6 9.6h3",
267
+ stroke: "currentColor", strokeWidth: 1.2, strokeLinecap: "round",
268
+ }),
269
+ h("path", {
270
+ d: "M12.5 9.9l.45 1.15 1.15.45-1.15.45-.45 1.15-.45-1.15-1.15-.45 1.15-.45z",
271
+ fill: "currentColor",
272
+ }),
273
+ );
241
274
  }
242
275
 
243
276
  function MainPanel() {
@@ -249,9 +282,21 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
249
282
  * 打开先到主页(#/home/decks),点 deck 后是左中右三栏:左原生 Chat / 中间整页图片 / 右资料库。
250
283
  * 对话仍走 DSH 原生 Chat(工作台左栏嵌 `/?decklineChat=1`),权限、附件、模型、思考过程、气泡都是原生的。
251
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();
252
296
  function WorkbenchFrame() {
253
297
  return h("iframe", {
254
298
  id: "dsh-deck-workbench-frame",
299
+ ref: (node) => setWorkbenchFrame(node),
255
300
  title: "Deckline 工作台",
256
301
  src: "/api/deck-studio.app#/home/decks",
257
302
  style: { width: "100%", height: "100%", border: 0, display: "block", background: "#faf8f2" },
@@ -261,9 +306,94 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
261
306
  });
262
307
  }
263
308
 
264
- /** 客户端插件体:样式 + 槽位注册。 */
265
- function apply(ctx) {
266
- injectStylesheet();
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
+ }
267
397
  // 工作台左栏会把 DSH 原生 Chat 以 `/?decklineChat=1` 嵌进来(同源 iframe)。
268
398
  // 那个嵌套的 DSH 实例不再自动打开工作台,否则会层层嵌套(F-68)。
269
399
  const chatEmbed = new URLSearchParams(window.location.search).get("decklineChat") === "1";
@@ -406,7 +536,11 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
406
536
  // 不把插件安装目录或 DSH 私有数据目录登记成用户工作区。
407
537
  const path = normalize(cwd);
408
538
  const internal = /(^|\/)node_modules(\/|$)|(^|\/)\.dsh(\/|$)/.test(path);
409
- if (path && path !== "/" && !internal) return register(cwd);
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);
410
544
  }
411
545
  const sourceRoot = String(data.sourceRoot ?? "").trim();
412
546
  const sourceWorkspace = sourceRoot ? containing(sourceRoot) : null;
@@ -461,7 +595,25 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
461
595
  let sessionId = String(data.sessionId ?? "").trim();
462
596
  if (data.action === "ensure") sessionId = await ensureConversationSession(data);
463
597
  if (!sessionId) throw new Error("没有目标会话");
464
- if (data.action === "open" || data.action === "ensure") uiWorkspace.openSession(sessionId);
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
+ }
465
617
  if (data.prompt && data.action === "ensure" && sessionId !== String(data.sessionId ?? "")) {
466
618
  await sendToSession(sessionId, String(data.prompt));
467
619
  }
@@ -475,6 +627,39 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
475
627
  return () => window.removeEventListener("message", onMessage);
476
628
  }, "deck-studio: conversation bridge");
477
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
+
478
663
  if (!chatEmbed) {
479
664
  /* 工作台顶栏「☰ DSH」→ 宿主:临时展开 / 收起左侧栏(需要看会话、插件、设置时用)。 */
480
665
  ctx.effect(() => {
@@ -535,17 +720,16 @@ div[data-sidebar-collapsed]{grid-template-columns:0px minmax(0px,1fr) 0px!import
535
720
  return () => window.clearTimeout(timer);
536
721
  }, "deck-studio: auto-open workbench");
537
722
 
538
- // 唯一入口:侧栏脚部一个「Deckline」按钮(重新打开工作台);→ 对话用工作台里的「← 回到对话」。
723
+ // 唯一入口:侧栏面板行「Deckline」(与 plugins / schedules 等插件同一种形态,带图标);
724
+ // → 对话用工作台里的「← 回到对话」。
539
725
  if (hasRightbar) {
540
- ctx.effect(
541
- () => ctx.slots.inject("sidebar.footer.action", () =>
542
- ctx.slots.register({ name: "sidebar.footer.action", id: `${PLUGIN_ID}-workbench` }, (props) => {
543
- const button = h("button", { onClick: openWorkbench, title: "打开 Deck 工作台(对话在 DSH 原生 Chat)", style: { cursor: "pointer" } },
544
- h("span", { style: { fontSize: 13 } }, "▤"), props?.wide ? h("span", { style: { marginLeft: 4 } }, "Deckline") : null);
545
- return button;
546
- })),
547
- "deck-studio: workbench footer button",
548
- );
726
+ if (layout?.selectPanel) {
727
+ ctx.effect(
728
+ () => ctx.slots.inject("sidebar.panellist", () =>
729
+ ctx.slots.register({ name: "sidebar.panellist", id: WORKBENCH_KEY, order: 46, label: () => "Deckline" }, DeckGlyph)),
730
+ "deck-studio: workbench sidebar entry",
731
+ );
732
+ }
549
733
  ctx.logger?.info?.("deck-studio client: workbench-only mode");
550
734
  } else {
551
735
  ctx.effect(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-deck-studio",
3
- "version": "0.2.12",
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": {
@@ -43,7 +43,7 @@ node -v && pnpm -v && dsh --version
43
43
  dsh --profile deckline --from-default-profile web
44
44
 
45
45
  # 2) install the plugin; dsh plugin add adds the bundle to the profile and enables it
46
- dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.12
46
+ dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
47
47
  ```
48
48
 
49
49
  > Do not skip step 1: the `deckline` profile is **never created automatically**. Running
@@ -181,7 +181,7 @@ From zero to 13090, run these three commands in order (options for steps 1–2 a
181
181
  dsh --profile deckline --from-default-profile web
182
182
 
183
183
  # 2) install the plugin (adds it to that profile and enables it)
184
- dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.12
184
+ dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
185
185
 
186
186
  # 3) start
187
187
  dsh --profile deckline --port 13090
@@ -45,7 +45,7 @@ node -v && pnpm -v && dsh --version
45
45
  dsh --profile deckline --from-default-profile web
46
46
 
47
47
  # 2) 装插件;dsh plugin add 会自动把组合包加入 profile 并默认启用
48
- dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.12
48
+ dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
49
49
  ```
50
50
 
51
51
  > 第 1 步不能省:`deckline` profile **不会**被自动创建。跳过第 1 步直接 `dsh --profile deckline --port 13090`,
@@ -54,7 +54,7 @@ dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.12
54
54
 
55
55
  > 国内网络直连 `registry.npmjs.org` 可能很慢(20 MB 包实测约 19 KB/s,要十几分钟)。换镜像会快很多:
56
56
  > ```sh
57
- > npm_config_registry=https://registry.npmmirror.com dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.12
57
+ > npm_config_registry=https://registry.npmmirror.com dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
58
58
  > ```
59
59
  > 或一次性 `npm config set registry https://registry.npmmirror.com`。
60
60
 
@@ -75,7 +75,7 @@ DSH 目前没有官方插件市场,市场是「索引源」模式:添加下
75
75
  https://raw.githubusercontent.com/yuna78/dsh-deck-studio/main/market-index.json
76
76
  ```
77
77
 
78
- 3. 搜索 `Deckline` 或 `PPT` → **安装**,会安装 `dsh-plugin-deck-studio@0.2.12`(索引里声明了国内镜像 registry)。
78
+ 3. 搜索 `Deckline` 或 `PPT` → **安装**,会安装 `dsh-plugin-deck-studio@0.2.13`(索引里声明了国内镜像 registry)。
79
79
  4. 之后同 A 的第 3 步启动。
80
80
 
81
81
  索引文件就是仓库根目录的 [`market-index.json`](../market-index.json)(格式 1),新版本发布后更新版本号即可。
@@ -210,7 +210,7 @@ profile "deckline" does not exist
210
210
  dsh --profile deckline --from-default-profile web
211
211
 
212
212
  # 2) 装插件(自动加进该 profile 并启用)
213
- dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.12
213
+ dsh plugin --profile deckline add dsh-plugin-deck-studio@0.2.13
214
214
 
215
215
  # 3) 启动
216
216
  dsh --profile deckline --port 13090
package/ui/workbench.css CHANGED
@@ -238,6 +238,44 @@ body.resizing{user-select:none;cursor:col-resize}
238
238
  .hn-label{flex:1}
239
239
  .home-nav-item em{font-style:normal;font-size:10px;color:#93a0a3;background:#fffdf8;border:1px solid var(--line);border-radius:9px;padding:0 5px}
240
240
  .home-nav-item.active em{color:var(--teal);border-color:#bfe0dc}
241
+ /* F-193/F-194:左栏「关于」单入口(版本 chip + 有新版小圆点) */
242
+ .home-nav-sep{height:1px;background:var(--line);margin:10px 4px 0}
243
+ .hn-dot{width:6px;height:6px;border-radius:50%;background:var(--gold);box-shadow:0 0 0 3px color-mix(in srgb,var(--gold) 18%,transparent)}
244
+ .hn-dot[hidden]{display:none}
245
+ /* F-194:「关于」单页——从上到下:作者 → 公众号 → 版本 → 开源 → 其他项目 */
246
+ .about-stack{display:flex;flex-direction:column;gap:12px;max-width:880px}
247
+ .about-card{border:1px solid var(--line);background:#fffdf8;border-radius:10px;padding:16px 18px;min-width:0}
248
+ .about-card h3{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;margin:0 0 8px;font-family:"Noto Serif SC",Georgia,serif;font-size:16px;color:var(--ink)}
249
+ .about-role{font-family:Inter,-apple-system,sans-serif;font-size:10.5px;font-weight:400;color:#93a0a3;letter-spacing:.02em}
250
+ .about-hero{display:flex;flex-direction:column;gap:14px;background:linear-gradient(180deg,#fffdf8 0%,#faf5e9 100%)}
251
+ .about-profile{display:flex;gap:14px;align-items:center;min-width:0}
252
+ .about-wechat{display:flex;align-items:center;gap:14px;border-top:1px solid var(--line);padding-top:13px}
253
+ .about-wechat-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
254
+ .about-wechat b{font-size:13px;color:var(--teal);line-height:1.5}
255
+ .about-wechat .eyebrow{color:#93a0a3}
256
+ .about-wechat .about-hint{margin:2px 0 0}
257
+ .about-wechat .about-qr{flex:0 0 auto;width:104px;max-width:104px;border-radius:7px}
258
+ .about-avatar{flex:0 0 auto;width:58px;height:58px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-family:"Noto Serif SC",Georgia,serif;font-size:24px;color:#fffdf8;background:linear-gradient(145deg,#0e5563,#08343d);box-shadow:inset 0 0 0 1px #ffffff22}
259
+ .about-profile-body{min-width:0;max-width:560px}
260
+ .about-desc{margin:0 0 8px;color:#5d7276;font-size:12px;line-height:1.65}
261
+ .about-line{margin:0 0 4px;color:#728086;font-size:11.5px}
262
+ .about-line b{color:var(--teal)}
263
+ .about-line code{font-size:11px;color:#5d7276;background:#f3efe4;border:1px solid var(--line);border-radius:5px;padding:1px 5px}
264
+ .about-hint{margin:8px 0 0;color:#93a0a3;font-size:10.5px;line-height:1.6;word-break:break-word}
265
+ .about-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
266
+ .about-actions .btn{font-size:11px}
267
+ .about-state{margin:8px 0 0;color:var(--gold)}
268
+ .about-qr{display:block;width:140px;max-width:140px;aspect-ratio:1;object-fit:contain;border:1px solid var(--line);border-radius:8px;background:#fff}
269
+ .about-qr-empty{display:flex;align-items:center;justify-content:center;width:140px;aspect-ratio:1;color:#93a0a3;font-size:11px;border:1px dashed var(--line);border-radius:8px;background:#f8f5ed;text-align:center;padding:8px}
270
+ .about-project-list{list-style:none;margin:2px 0 0;padding:0}
271
+ .about-project-row{display:flex;gap:14px;align-items:flex-start;justify-content:space-between;padding:12px 0;border-top:1px solid var(--line)}
272
+ .about-project-row:first-child{border-top:0;padding-top:2px}
273
+ .about-project-main{flex:1;min-width:0}
274
+ .about-project-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
275
+ .about-project-head b{font-size:13px;color:var(--ink)}
276
+ .about-badge{flex:0 0 auto;font-size:10px;color:var(--teal);background:#e2f1ef;border:1px solid #bfe0dc;border-radius:9px;padding:1px 7px}
277
+ .about-project-row .about-desc{margin:4px 0 0}
278
+ .about-project-row .btn{flex:0 0 auto}
241
279
  .home-main{padding:26px 32px;overflow-y:auto;min-width:0}
242
280
  .home-greet{padding-bottom:16px;border-bottom:1px solid var(--line);margin-bottom:18px}
243
281
  .home-greet h1{margin:5px 0 4px;font-family:"Noto Serif SC",Georgia,serif;font-size:30px;color:var(--ink);letter-spacing:.01em}
@@ -441,6 +479,15 @@ body.has-chat .layout{grid-template-columns:var(--nav-w,430px) 5px minmax(0,1fr)
441
479
  .chat-session-item span{font-size:9.5px;color:#8a9599}
442
480
  .chat-session-empty{margin:6px 8px 8px;font-size:10.5px;color:#8a9599;line-height:1.6}
443
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}
444
491
 
445
492
  /* 叙事结构概览(脚本页顶部,从 script 派生) */
446
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
@@ -134,6 +134,11 @@
134
134
  <button class="home-nav-item" data-section="resources"><span class="hn-icon">◇</span><span class="hn-label">资源库</span><em id="navCountResources">0</em></button>
135
135
  <button class="home-nav-item" data-section="conversations"><span class="hn-icon">☰</span><span class="hn-label">Conversations</span><em id="navCountConvs">0</em></button>
136
136
  <button class="home-nav-item" data-section="manual"><span class="hn-icon">?</span><span class="hn-label">使用手册</span></button>
137
+ <!-- F-193/F-194:关于只留一页入口;版本 chip + 有新版小圆点仍常驻,升级入口不藏在顶栏图标里 -->
138
+ <div class="home-nav-sep" aria-hidden="true"></div>
139
+ <button class="home-nav-item" data-section="about" id="aboutNavBtn" title="关于 Deckline:版本 / 公众号 / 开源地址 / 其他项目">
140
+ <span class="hn-icon">☺</span><span class="hn-label">关于</span><em id="navUpdateVer">—</em><i class="hn-dot" id="navUpdateDot" hidden></i>
141
+ </button>
137
142
  </nav>
138
143
  </aside>
139
144
  <main class="home-main">
@@ -281,6 +286,11 @@
281
286
  </div>
282
287
  <div class="chat-session-menu" id="chatSessionMenu" hidden></div>
283
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>
284
294
  <iframe id="nativeChatFrame" title="DSH 原生对话"></iframe>
285
295
  </aside>
286
296
  <div class="splitter" data-split="nav" title="拖动调整对话栏宽度" hidden></div>
@@ -578,6 +588,14 @@ let SETTINGS_FORM_DIRTY = false;
578
588
 
579
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: [] };
580
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";
581
599
 
582
600
  const esc = (v) => String(v ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
583
601
  const getEl = (id) => document.getElementById(id);
@@ -1449,8 +1467,8 @@ async function copyDataDirDiagnostics() {
1449
1467
 
1450
1468
  /* ---------------- 首页(左侧导航 + 右侧分区 blocks) ---------------- */
1451
1469
 
1452
- const HOME_SECTIONS = ["decks", "designs", "resources", "conversations", "manual"];
1453
- const HOME_TITLES = { decks: "Decks", designs: "Designs", resources: "资源库", conversations: "Conversations" };
1470
+ const HOME_SECTIONS = ["decks", "designs", "resources", "conversations", "manual", "about"];
1471
+ const HOME_TITLES = { decks: "Decks", designs: "Designs", resources: "资源库", conversations: "Conversations", manual: "使用手册", about: "关于" };
1454
1472
 
1455
1473
  function homeDateText() {
1456
1474
  const now = new Date();
@@ -1531,6 +1549,7 @@ function renderHome() {
1531
1549
  resources: `${resources.length} 个资源(logo / 素材);点开预览。`,
1532
1550
  conversations: `${sessions.length} 个历史会话;点一个继续聊。`,
1533
1551
  manual: "从新建到导出:截图 + 步骤;找不到就直接在 Chat 里问。",
1552
+ about: "作者 / 公众号 / 版本更新 / 开源地址 / 其他项目,都在这一页。",
1534
1553
  }[section];
1535
1554
  getEl("homeGreetSub").textContent = greet;
1536
1555
 
@@ -1550,8 +1569,22 @@ function renderHome() {
1550
1569
  if (STATE.manualDoc) openManualDoc(box, STATE.manualDoc);
1551
1570
  else renderManualHome(box);
1552
1571
  }
1572
+ else if (section === "about") {
1573
+ box.innerHTML = renderHomeAbout(resources);
1574
+ paintUpdate(); // 版本 chip / 更新状态在关于页也要跟着顶栏走(F-193)
1575
+ }
1553
1576
  else box.innerHTML = renderHomeConversations(sessions, bindings);
1554
1577
 
1578
+ // F-193:「关于」分区里的按钮(检查更新 / 外链)——外链直接开新标签。
1579
+ box.querySelectorAll("[data-about-check]").forEach((node) => node.addEventListener("click", () => checkPluginUpdate()));
1580
+ box.querySelectorAll("[data-about-external]").forEach((node) => node.addEventListener("click", () => {
1581
+ const links = aboutLinks();
1582
+ const kind = node.dataset.aboutExternal;
1583
+ if (kind === "repo") openExternalUrl(links.repo, "还没拿到仓库地址");
1584
+ else if (kind === "issues") openExternalUrl(links.issues, "还没拿到提建议地址");
1585
+ else if (kind === "profile") openExternalUrl(links.profile, "还没拿到 GitHub 主页地址");
1586
+ }));
1587
+
1555
1588
  box.querySelectorAll("[data-home-deck]").forEach((node) => node.addEventListener("click", async () => {
1556
1589
  const deckId = node.dataset.homeDeck;
1557
1590
  // 先等 deck 切完(iframe 里的 Chat 也要加载完),再选默认会话——否则 postMessage 发早了会落到旧会话。
@@ -1711,6 +1744,90 @@ function renderHomeResources(resources) {
1711
1744
  </button>`).join("")}</div>`;
1712
1745
  }
1713
1746
 
1747
+ /* ---------------- F-193 / F-194:首页「关于」单页(作者 → 公众号 → 版本 → 开源 → 其他项目) ---------------- */
1748
+
1749
+ /** 公众号二维码从资源库现取(个人二维码不进仓库);拿不到就降级成说明,不显示破图。 */
1750
+ function aboutWechatQr(resources = []) {
1751
+ return resources.find((resource) => /二维码|qr|微信|wechat/i.test(`${resource?.name || ""} ${resource?.description || ""} ${resource?.url || ""}`)) || null;
1752
+ }
1753
+
1754
+ function renderHomeAbout(resources = []) {
1755
+ const links = aboutLinks();
1756
+ const qr = aboutWechatQr(resources);
1757
+ const versionLine = UPDATE.current ? `v${UPDATE.current}` : "读取中…";
1758
+ const installNote = UPDATE.installMode === "linked"
1759
+ ? "本机是开发链接安装(link:/file:):更新走仓库,界面不执行 pnpm。"
1760
+ : UPDATE.installMode === "npm"
1761
+ ? "npm 安装:有新版可以在这里直接更新,重启 DSH 生效。"
1762
+ : "没识别出安装方式时,可手动 dsh plugin add。";
1763
+ const repoShort = links.repo.replace(/^https?:\/\//, "");
1764
+ const projectRows = [
1765
+ { name: "Deckline · dsh-deck-studio", badge: "本插件", external: "repo", desc: "DSH 的 image deck / PPT 工作台:故事线、逐页 prompt、AI 出图与版本留存、风格库、导出 PDF/PPTX。" },
1766
+ ];
1767
+ if (links.profile) {
1768
+ projectRows.push({
1769
+ name: `GitHub @${links.profile.split("/").pop()}`,
1770
+ badge: "全部项目",
1771
+ external: "profile",
1772
+ desc: "我的开源主页:插件、实验和工具会陆续挂在这里。",
1773
+ });
1774
+ }
1775
+ return `<div class="section-head"><h2>关于</h2><span class="small">作者 · 公众号 · 版本更新 · 开源</span></div>
1776
+ <div class="about-stack">
1777
+ <section class="about-card about-hero">
1778
+ <div class="about-profile">
1779
+ <div class="about-avatar" aria-hidden="true">Y</div>
1780
+ <div class="about-profile-body">
1781
+ <h3>${esc(ABOUT.author)}<span class="about-role">${esc(ABOUT.role)}</span></h3>
1782
+ <p class="about-desc">${esc(ABOUT.intro)}</p>
1783
+ </div>
1784
+ </div>
1785
+ <div class="about-wechat">
1786
+ <div class="about-wechat-text">
1787
+ <div class="eyebrow">微信公众号</div>
1788
+ <b>${esc(ABOUT.account)}</b>
1789
+ <p class="about-hint">${qr ? "扫码关注 / 加我微信" : `把二维码图片放进数据目录的 <code>resources/</code>(资源库),这里会自动显示。`}</p>
1790
+ </div>
1791
+ ${qr
1792
+ ? `<img class="about-qr" src="${esc(qr.url)}" alt="${esc(qr.name || "公众号二维码")}" loading="lazy">`
1793
+ : `<div class="about-qr about-qr-empty">二维码未收录</div>`}
1794
+ </div>
1795
+ </section>
1796
+
1797
+ <section class="about-card">
1798
+ <h3>版本与更新</h3>
1799
+ <p class="about-line">当前版本 <b>${esc(versionLine)}</b></p>
1800
+ <p class="about-desc">${esc(installNote)}</p>
1801
+ <div class="about-actions">
1802
+ <button class="btn" data-about-check>检查新版本</button>
1803
+ </div>
1804
+ <p class="small about-state" data-about-state></p>
1805
+ </section>
1806
+ <section class="about-card">
1807
+ <h3>开源地址与反馈</h3>
1808
+ <p class="about-line"><code>${esc(repoShort)}</code></p>
1809
+ <p class="about-desc">用着不对、想要什么功能,直接提 issue;仓库里能看到版本记录与路线。</p>
1810
+ <div class="about-actions">
1811
+ <button class="btn ghost" data-about-external="repo">仓库地址</button>
1812
+ <button class="btn alt" data-about-external="issues">提交建议 / 问题</button>
1813
+ </div>
1814
+ </section>
1815
+ <section class="about-card">
1816
+ <h3>其他开源项目</h3>
1817
+ <ul class="about-project-list">${projectRows.map((row) => `
1818
+ <li class="about-project-row">
1819
+ <div class="about-project-main">
1820
+ <div class="about-project-head"><b>${esc(row.name)}</b><span class="about-badge">${esc(row.badge)}</span></div>
1821
+ <p class="about-desc">${esc(row.desc)}</p>
1822
+ </div>
1823
+ <button class="btn ghost" data-about-external="${esc(row.external)}">打开</button>
1824
+ </li>`).join("")}
1825
+ </ul>
1826
+ <p class="about-hint">最新列表以 GitHub 主页为准;想第一时间看到更新,关注公众号「${esc(ABOUT.account)}」。</p>
1827
+ </section>
1828
+ </div>`;
1829
+ }
1830
+
1714
1831
  function renderHomeConversations(sessions, bindings = []) {
1715
1832
  const bound = new Map((bindings || []).map((binding) => [binding.sessionId, binding]));
1716
1833
  const targetLabel = (binding) => {
@@ -1813,19 +1930,55 @@ function dlToast(text, ms = 4200) {
1813
1930
  /** F-188:更新状态(版本 / 安装方式 / 仓库链接)由 workbench payload 带下来。 */
1814
1931
  const UPDATE = { current: "", latest: "", hasUpdate: false, installMode: "", repo: "", suggest: "", note: "" };
1815
1932
 
1933
+ /** F-193:左栏「关于」分组用的作者 / 公众号信息;仓库链接优先用包里的 repository,兜底写死公开地址。 */
1934
+ const ABOUT = {
1935
+ author: "Yuna",
1936
+ role: "Deckline 作者",
1937
+ account: "Yuna 的 AI 修炼手册",
1938
+ intro: "把 AI 落到工作现场:用对话把分享、培训、汇报做成整页图片 deck。",
1939
+ fallbackRepo: "https://github.com/yuna78/dsh-deck-studio",
1940
+ };
1941
+
1942
+ function aboutLinks() {
1943
+ const repo = UPDATE.repo || ABOUT.fallbackRepo;
1944
+ const issues = UPDATE.suggest || UPDATE.issues || `${repo.replace(/\/+$/, "")}/issues/new/choose`;
1945
+ const profile = repo.replace(/^https?:\/\/(?:www\.)?github\.com\/([^/]+).*$/i, "https://github.com/$1");
1946
+ return { repo, issues, profile: profile === repo ? "" : profile };
1947
+ }
1948
+
1949
+ /** 外链统一走这里:拿不到地址时给 toast,不让按钮静默没反应。 */
1950
+ function openExternalUrl(url, missingMessage) {
1951
+ if (!url) {
1952
+ dlToast(missingMessage || "链接还没拿到:插件包或运行环境里没有配置地址", 5000);
1953
+ return;
1954
+ }
1955
+ window.open(url, "_blank", "noopener");
1956
+ }
1957
+
1816
1958
  function paintUpdate() {
1817
1959
  const ver = getEl("updateVer");
1818
1960
  if (ver) ver.textContent = UPDATE.current ? `v${UPDATE.current}` : "—";
1819
1961
  const dot = getEl("updateDot");
1820
1962
  if (dot) dot.hidden = !UPDATE.hasUpdate;
1963
+ const title = UPDATE.hasUpdate
1964
+ ? `有新版 v${UPDATE.latest}(当前 v${UPDATE.current})`
1965
+ : UPDATE.installMode === "linked"
1966
+ ? "本机是开发链接安装:更新走 git pull"
1967
+ : "检查更新";
1821
1968
  const btn = getEl("updateBtn");
1822
- if (btn) {
1823
- btn.title = UPDATE.hasUpdate
1824
- ? `有新版 v${UPDATE.latest}(当前 v${UPDATE.current})`
1825
- : UPDATE.installMode === "linked"
1826
- ? "本机是开发链接安装:更新走 git pull"
1827
- : "检查更新";
1828
- }
1969
+ if (btn) btn.title = title;
1970
+ // F-193:左栏「检查新版本」与顶栏 ↻ 共用同一份状态(版本 chip + 有新版小圆点)
1971
+ const navVer = getEl("navUpdateVer");
1972
+ if (navVer) navVer.textContent = UPDATE.current ? `v${UPDATE.current}` : "—";
1973
+ const navDot = getEl("navUpdateDot");
1974
+ if (navDot) navDot.hidden = !UPDATE.hasUpdate;
1975
+ const aboutNavBtn = getEl("aboutNavBtn");
1976
+ if (aboutNavBtn) aboutNavBtn.title = UPDATE.hasUpdate ? `关于 Deckline · ${title}` : "关于 Deckline:版本 / 公众号 / 开源地址 / 其他项目";
1977
+ const aboutState = document.querySelectorAll("[data-about-state]");
1978
+ aboutState.forEach((node) => {
1979
+ // 只在「有新版」时占用这行;「当前版本」上面已经写过了,别重复。
1980
+ node.textContent = UPDATE.hasUpdate ? `发现新版 v${UPDATE.latest}(当前 v${UPDATE.current})` : "";
1981
+ });
1829
1982
  }
1830
1983
 
1831
1984
  function absorbUpdate(payload) {
@@ -4758,6 +4911,7 @@ function decorateNativeChat(doc) {
4758
4911
 
4759
4912
  function setupNativeChat() {
4760
4913
  if (EMBED) return; // embed 模式下宿主 shell 本身就有原生 Chat
4914
+ if (DESKTOP_HOST) return setupHostChatColumn(); // F-202B:桌面不嵌 DSH 页面
4761
4915
  const column = getEl("chatCol");
4762
4916
  const frame = getEl("nativeChatFrame");
4763
4917
  const splitter = document.querySelector('.splitter[data-split="nav"]');
@@ -4797,6 +4951,92 @@ function setupNativeChat() {
4797
4951
  });
4798
4952
  }
4799
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
+
4800
5040
  function copyText(text) {
4801
5041
  try {
4802
5042
  navigator.clipboard.writeText(text);
@@ -4806,13 +5046,13 @@ function copyText(text) {
4806
5046
  }
4807
5047
  }
4808
5048
 
4809
- /** 工作台 → 原生 Chat 的窗口:非 embed 用左栏自己的 iframe,embed 用宿主 DSH 壳。 */
5049
+ /** 工作台 → 原生 Chat 的窗口:非 embed 用左栏自己的 iframe;embed / 桌面用宿主 DSH 壳。 */
4810
5050
  function nativeChatWindow() {
4811
- if (!EMBED) {
5051
+ if (!EMBED && !DESKTOP_HOST) {
4812
5052
  const frame = getEl("nativeChatFrame");
4813
5053
  return frame && frame.contentWindow ? frame.contentWindow : null;
4814
5054
  }
4815
- return window.parent !== window ? window.parent : null; // embed:宿主 DSH 壳
5055
+ return window.parent !== window ? window.parent : null; // embed / 桌面:宿主 DSH 壳
4816
5056
  }
4817
5057
 
4818
5058
  function requestNativeChat(text) {
@@ -4868,6 +5108,9 @@ function requestConversation(payload) {
4868
5108
  workspacePath: STATE.data?.workspacePath || "",
4869
5109
  sourceRoot: STATE.data?.active?.sourceRoot || "",
4870
5110
  ...payload,
5111
+ // F-202B:桌面 Dock 模式默认不抢主区(open:false);显式 takeMain → open:true 切宿主主区。
5112
+ // web 不传 open(undefined → 宿主按原语义 openSession)。
5113
+ open: DESKTOP_HOST ? payload.takeMain === true : undefined,
4871
5114
  }, "*");
4872
5115
  setTimeout(() => {
4873
5116
  if (settled) return;
@@ -4935,6 +5178,8 @@ async function ensureDeckConversation(deckId) {
4935
5178
  kind: "deck",
4936
5179
  id: deckId,
4937
5180
  title: label,
5181
+ // 桌面:点 deck 是进工作台,不是切出去聊天——后台 ensure,不切换宿主视图。
5182
+
4938
5183
  prompt: `本对话绑定 deck ${deckId}(${label})。后续所有修改都在这个 deck 上进行;先读 intent 与 script 概要,等我的指令,不要直接出图。`,
4939
5184
  });
4940
5185
  if (!result.ok) {
@@ -4987,6 +5232,12 @@ async function openBoundConversation(sessionId) {
4987
5232
  getEl("homeGreetSub").textContent = `没能切到会话:${result.reason || "未知原因"}`;
4988
5233
  return;
4989
5234
  }
5235
+ if (DESKTOP_HOST) {
5236
+ // 桌面:用户点会话就是要对话;宿主主区已切到该会话,收起工作台(回工作台时再对齐到绑定的 deck)。
5237
+ STATE.chatSessionId = sessionId;
5238
+ showDeck(binding?.kind === "deck" ? binding.id : null);
5239
+ return;
5240
+ }
4990
5241
  if (binding?.kind === "deck") {
4991
5242
  showDeck(binding.id);
4992
5243
  return;
@@ -5030,8 +5281,11 @@ function activeChatTarget() {
5030
5281
  return chatTargetForBinding({ kind: "deck", id: active.deckId });
5031
5282
  }
5032
5283
 
5033
- /** 当前 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 被它顶掉)。 */
5034
5287
  function currentChatSessionId() {
5288
+ if (DESKTOP_HOST) return STATE.chatSessionId || "";
5035
5289
  try {
5036
5290
  const win = getEl("nativeChatFrame")?.contentWindow;
5037
5291
  const raw = win?.localStorage?.getItem("dsh.sessions.current");
@@ -5144,6 +5398,7 @@ function renderChatSessionMenu(sessions, target) {
5144
5398
  STATE.chatSessionId = sessionId;
5145
5399
  await refreshSessions();
5146
5400
  renderChatHeader();
5401
+ // 桌面:会话已在宿主主区打开,把工作台面板让出来(web 版留在内嵌 iframe 里)。
5147
5402
  }));
5148
5403
  }
5149
5404
 
@@ -5342,6 +5597,9 @@ async function newChatForTarget() {
5342
5597
  try {
5343
5598
  const result = await requestConversation({ action: "ensure", kind: target.kind, id: target.id, title: target.label });
5344
5599
  if (!result.ok || !result.sessionId) {
5600
+ // F-202B:失败要用户可见——之前只写按钮 title,Dock 模式下看起来像「点了没反应」。
5601
+ getEl("chatBindChip").textContent = "会话新建失败";
5602
+ getEl("chatBindChip").title = `新建会话失败:${result.reason || "未知原因"};点这里重试`;
5345
5603
  getEl("chatSessionBtn").title = `新建会话失败:${result.reason || "未知原因"}`;
5346
5604
  return;
5347
5605
  }
@@ -5349,6 +5607,7 @@ async function newChatForTarget() {
5349
5607
  await bindConversation({ sessionId: result.sessionId, kind: target.kind, id: target.id, title: target.label });
5350
5608
  await refreshSessions();
5351
5609
  renderChatHeader();
5610
+ // 桌面:新建就是要点开聊——切到宿主主区的原生会话。
5352
5611
  } finally {
5353
5612
  button.disabled = false;
5354
5613
  }
@@ -5621,7 +5880,7 @@ function backToManual(box) {
5621
5880
  getEl("updateBtn").addEventListener("click", checkPluginUpdate);
5622
5881
  getEl("repoBtn").addEventListener("click", openRepo);
5623
5882
  await refreshSessions();
5624
- getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : "对话在左侧 · DSH 原生";
5883
+ getEl("save").textContent = EMBED ? "对话在 DSH 原生 Chat" : DESKTOP_HOST ? "对话 · 原生(桌面 Dock)" : "对话在左侧 · DSH 原生";
5625
5884
  // F-69:只有 profile 开了 hideShellSidebar,宿主左侧栏才会默认收起;此时顶栏给一个切换入口。
5626
5885
  if (!EMBED && window.parent !== window && STATE.data?.hideShellSidebar === true) {
5627
5886
  const shellBtn = getEl("shellSidebarBtn");
@@ -5636,7 +5895,7 @@ function backToManual(box) {
5636
5895
  getEl("workspaceView").style.display = "grid";
5637
5896
  switchChatToBoundSession().catch(() => {});
5638
5897
  } else {
5639
- const match = location.hash.match(/^#\/home\/(decks|designs|resources|conversations|manual)/);
5898
+ const match = location.hash.match(/^#\/home\/(decks|designs|resources|conversations|manual|about)/);
5640
5899
  STATE.homeSection = match ? match[1] : "decks";
5641
5900
  showHome();
5642
5901
  }