dsh-focus-overlay 0.3.0 → 0.5.0

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/README.en.md CHANGED
@@ -57,7 +57,7 @@ Once in focus mode, an assistant turn no longer shows every step — it folds in
57
57
  | Back to latest | A centered "↓ back to latest" floating button appears when you scroll away from the bottom |
58
58
  | File mentions | Wires `chatFileMentions`, turning inline-code tokens that name real files into clickable links |
59
59
  | i18n | Chinese / English copy, follows the UI language |
60
- | Settings page | Navbar toggle, open-position strategy, text-area width — persisted to `localStorage` |
60
+ | Plugin configuration card | A collapsible card under "Settings → Plugins → Plugin configuration": navbar toggle, open-position strategy, text-area width — persisted to `localStorage` |
61
61
  | Auto-enter focus | After a reply **completes normally**, auto-open focus at your question; abnormal endings (stop / error / max-tokens / interrupt) never fire |
62
62
  | Reply / waiting reminders | In focus, a "New reply ready + View" toast on completion; an "AI is waiting for your reply + Reply" toast on a question/approval, auto-cleared once answered |
63
63
 
@@ -72,6 +72,14 @@ dsh plugin --profile web add dsh-focus-overlay
72
72
  dsh web
73
73
  ```
74
74
 
75
+ > If `add` doesn't install the latest version, pnpm's `minimumReleaseAge` (minimum release age) security policy is at work: by default it won't resolve a version as `latest` within **24 hours** of publication, falling back to the previous stable release. To install the latest immediately, pin the version explicitly:
76
+ >
77
+ > ```sh
78
+ > dsh plugin --profile web add dsh-focus-overlay@<version>
79
+ > ```
80
+ >
81
+ > Or wait 24 hours, then re-run the plain `add` command.
82
+
75
83
  **From GitHub**
76
84
 
77
85
  ```sh
@@ -88,13 +96,6 @@ dsh web
88
96
  >
89
97
  > Then re-run `add`. **This authorization lets the package's code run on your machine at install time** — only grant it to sources you trust, and pin the commit (`github:boogoo619/dsh-focus-overlay#<sha>`).
90
98
 
91
- **Local directory / tarball**
92
-
93
- ```sh
94
- dsh plugin --profile web add ./dsh-focus-overlay # local directory
95
- dsh plugin --profile web add ./dsh-focus-overlay-0.1.0.tgz # tarball
96
- ```
97
-
98
99
  Restart `dsh web` after install.
99
100
 
100
101
  ## Usage
@@ -106,7 +107,7 @@ Restart `dsh web` after install.
106
107
 
107
108
  ## Settings
108
109
 
109
- Sidebar "Settings → Focus Mode":
110
+ In the sidebar "Settings → Plugins → Plugin configuration", expand the "Focus Mode" card:
110
111
 
111
112
  | Option | Description |
112
113
  | --- | --- |
package/README.md CHANGED
@@ -55,7 +55,7 @@
55
55
  | 回到最新 | 离开底部时显示居中的「↓ 回到最新」悬浮按钮 |
56
56
  | 文件提及 | 接 `chatFileMentions`,内联代码命中真实文件时变成可点链接 |
57
57
  | i18n | 中 / 英文案,跟随界面语言 |
58
- | 设置页 | 导航条开关、打开定位策略、文字区宽度,持久化到 `localStorage` |
58
+ | 插件配置卡片 | 在「设置 → 插件 → 插件配置」中的可折叠卡片,含导航条开关、打开定位策略、文字区宽度,持久化到 `localStorage` |
59
59
  | 自动进入专注 | 回复**正常完成**后自动打开专注并定位到本轮你的提问;异常结束(停止 / 报错 / 超 token / 打断)不触发 |
60
60
  | 回复 / 等待提醒 | 专注中回复完成弹「新回复已生成 + 查看」;AI 提问 / 审批等待时弹「AI 正在等待你的回复 + 去回复」,回答完自动消失 |
61
61
 
@@ -70,6 +70,14 @@ dsh plugin --profile web add dsh-focus-overlay
70
70
  dsh web
71
71
  ```
72
72
 
73
+ > 如果 `add` 装到的不是最新版本:这是 pnpm 的 `minimumReleaseAge`(最小发布年龄)安全机制在起作用——默认 **24 小时**内不会把刚发布的版本当作 `latest` 解析,而是回退到上一个稳定版。想立即装最新版,显式指定版本号即可:
74
+ >
75
+ > ```sh
76
+ > dsh plugin --profile web add dsh-focus-overlay@<版本号>
77
+ > ```
78
+ >
79
+ > 或者等满 24 小时,再执行不带版本号的 `add` 命令。
80
+
73
81
  **从 GitHub**
74
82
 
75
83
  ```sh
@@ -86,13 +94,6 @@ dsh web
86
94
  >
87
95
  > 然后重新执行 `add`。**该授权允许本包代码在安装时于你的机器上执行**——请只对可信来源授权,并锁定 commit(`github:boogoo619/dsh-focus-overlay#<sha>`)。
88
96
 
89
- **本地目录 / tarball**
90
-
91
- ```sh
92
- dsh plugin --profile web add ./dsh-focus-overlay # 本地目录
93
- dsh plugin --profile web add ./dsh-focus-overlay-0.1.0.tgz # tarball
94
- ```
95
-
96
97
  安装后重启 `dsh web` 生效。
97
98
 
98
99
  ## 使用
@@ -104,7 +105,7 @@ dsh plugin --profile web add ./dsh-focus-overlay-0.1.0.tgz # tarball
104
105
 
105
106
  ## 设置
106
107
 
107
- 侧栏「设置 → 专注模式」:
108
+ 侧栏「设置 → 插件 → 插件配置」中,展开「专注模式」卡片:
108
109
 
109
110
  | 选项 | 说明 |
110
111
  | --- | --- |
package/lib/client.js CHANGED
@@ -264,15 +264,26 @@ window.__ModuleLoader__.load({
264
264
  return -1;
265
265
  }
266
266
  /**
267
- * Decide whether an already-settled conversation snapshot (`running === false`)
268
- * ends in a *normal* completed reply, and which user message started it.
267
+ * Decide whether a settled conversation snapshot (`running === false`) ends in
268
+ * a *normal* completed reply, and which user message started it.
269
269
  *
270
- * The caller owns the `running true → false` edge; this is the pure
271
- * "is the tail a finished reply?" predicate: it walks backwards to the first
272
- * turn-terminating node (`assistant` / `turn-error` / `turn-max-tokens`), then
273
- * accepts only a finalized, non-interrupted assistant node.
270
+ * The caller owns the `running true → false` edge. `running` is relayed on a
271
+ * different stream than the conversation events, so the edge can fire before an
272
+ * aborted turn's `interrupted` node has landed at that instant the tail is
273
+ * still held in `partial` and `nodes` is missing the final assistant (a
274
+ * finalized question message can then be mistaken for the tail). To close that
275
+ * race we require the snapshot to be *stable* first: while `partial` is non-null
276
+ * we return `settled: false` and the caller must re-evaluate on a later
277
+ * snapshot. Once stable, this walks backwards to the first turn-terminating node
278
+ * (`assistant` / `turn-error` / `turn-max-tokens`) and accepts only a finalized,
279
+ * non-interrupted assistant node.
274
280
  */
275
281
  function detectSettledCompletion(snap) {
282
+ if (snap && snap.partial != null) return {
283
+ settled: false,
284
+ completed: false,
285
+ anchorSeq: null
286
+ };
276
287
  const nodes = snap && snap.nodes ? snap.nodes : [];
277
288
  let terminal = null;
278
289
  for (let i = nodes.length - 1; i >= 0; i--) {
@@ -284,10 +295,12 @@ window.__ModuleLoader__.load({
284
295
  }
285
296
  }
286
297
  if (!terminal || terminal.kind !== "assistant" || terminal.interrupted) return {
298
+ settled: true,
287
299
  completed: false,
288
300
  anchorSeq: null
289
301
  };
290
302
  return {
303
+ settled: true,
291
304
  completed: true,
292
305
  anchorSeq: lastUserSeq(nodes)
293
306
  };
@@ -739,51 +752,112 @@ window.__ModuleLoader__.load({
739
752
  children: t("toggle")
740
753
  });
741
754
  }
742
- function FocusSettings({ t }) {
755
+ function FocusSettingsCard({ t }) {
743
756
  const prefs = usePrefs();
744
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
745
- className: "fm-settings",
746
- children: [
747
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
748
- className: "fm-setting-row",
749
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
750
- type: "checkbox",
751
- checked: prefs.navbar,
752
- onChange: (e) => prefsStore.update({ navbar: e.target.checked })
753
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settings.navbar") })]
754
- }),
755
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
756
- className: "fm-setting-row",
757
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
758
- type: "checkbox",
759
- checked: prefs.scroll === "preserve",
760
- onChange: (e) => prefsStore.update({ scroll: e.target.checked ? "preserve" : "bottom" })
761
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settings.scrollPreserve") })]
762
- }),
763
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
764
- className: "fm-setting-row",
765
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
766
- type: "checkbox",
767
- checked: prefs.autoFocus,
768
- onChange: (e) => prefsStore.update({ autoFocus: e.target.checked })
769
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settings.autoFocus") })]
770
- }),
771
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
772
- className: "fm-setting-row",
773
- children: [
774
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settings.width") }),
775
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
776
- type: "range",
777
- min: 480,
778
- max: 1200,
779
- step: 40,
780
- value: prefs.width,
781
- onChange: (e) => prefsStore.update({ width: Number(e.target.value) })
782
- }),
783
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [prefs.width, "px"] })
784
- ]
785
- })
786
- ]
757
+ const [open, setOpen] = (0, react.useState)(false);
758
+ const title = t("settings.label");
759
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
760
+ className: "fm-plugin-card" + (open ? " fm-plugin-card-open" : ""),
761
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
762
+ type: "button",
763
+ className: "fm-plugin-card-header",
764
+ "aria-expanded": open,
765
+ "aria-label": `${t(open ? "settings.collapse" : "settings.expand")}: ${title}`,
766
+ onClick: () => setOpen(!open),
767
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
768
+ className: "fm-plugin-card-headtext",
769
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
770
+ className: "fm-plugin-card-name",
771
+ children: title
772
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
773
+ className: "fm-plugin-card-desc",
774
+ children: t("settings.description")
775
+ })]
776
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: "fm-plugin-card-chevron" + (open ? " fm-plugin-card-chevron-open" : "") })]
777
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
778
+ className: "fm-plugin-card-body",
779
+ children: [
780
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
781
+ className: "fm-plugin-field",
782
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
783
+ className: "fm-plugin-check",
784
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
785
+ type: "checkbox",
786
+ checked: prefs.navbar,
787
+ onChange: (e) => prefsStore.update({ navbar: e.target.checked })
788
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
789
+ className: "fm-plugin-check-label",
790
+ children: t("settings.navbar")
791
+ })]
792
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
793
+ className: "fm-plugin-field-hint",
794
+ children: t("settings.navbar.hint")
795
+ })]
796
+ }),
797
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
798
+ className: "fm-plugin-field",
799
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
800
+ className: "fm-plugin-check",
801
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
802
+ type: "checkbox",
803
+ checked: prefs.scroll === "preserve",
804
+ onChange: (e) => prefsStore.update({ scroll: e.target.checked ? "preserve" : "bottom" })
805
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
806
+ className: "fm-plugin-check-label",
807
+ children: t("settings.scrollPreserve")
808
+ })]
809
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
810
+ className: "fm-plugin-field-hint",
811
+ children: t("settings.scrollPreserve.hint")
812
+ })]
813
+ }),
814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
815
+ className: "fm-plugin-field",
816
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
817
+ className: "fm-plugin-check",
818
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
819
+ type: "checkbox",
820
+ checked: prefs.autoFocus,
821
+ onChange: (e) => prefsStore.update({ autoFocus: e.target.checked })
822
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
823
+ className: "fm-plugin-check-label",
824
+ children: t("settings.autoFocus")
825
+ })]
826
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
827
+ className: "fm-plugin-field-hint",
828
+ children: t("settings.autoFocus.hint")
829
+ })]
830
+ }),
831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
832
+ className: "fm-plugin-field",
833
+ children: [
834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
835
+ className: "fm-plugin-field-head",
836
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
837
+ className: "fm-plugin-field-label",
838
+ children: t("settings.width")
839
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
840
+ className: "fm-plugin-field-value",
841
+ children: [prefs.width, "px"]
842
+ })]
843
+ }),
844
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
845
+ type: "range",
846
+ className: "fm-plugin-range",
847
+ min: 480,
848
+ max: 1200,
849
+ step: 40,
850
+ value: prefs.width,
851
+ onChange: (e) => prefsStore.update({ width: Number(e.target.value) })
852
+ }),
853
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
854
+ className: "fm-plugin-field-hint",
855
+ children: t("settings.width.hint")
856
+ })
857
+ ]
858
+ })
859
+ ]
860
+ }) : null]
787
861
  });
788
862
  }
789
863
  //#endregion
@@ -820,10 +894,27 @@ window.__ModuleLoader__.load({
820
894
  .fm-reply-toast{position:absolute;left:50%;transform:translateX(-50%);bottom:64px;z-index:2;display:flex;align-items:center;gap:10px;background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:100px;padding:8px 8px 8px 16px;box-shadow:0 4px 16px rgba(0,0,0,.12);font-size:13px}
821
895
  .fm-reply-toast-dot{width:8px;height:8px;border-radius:999px;background:var(--dsw-static-deepseek-500,#4176e6);flex:0 0 auto}
822
896
  .fm-reply-toast-text{white-space:nowrap}
823
- .fm-settings{display:flex;flex-direction:column;gap:16px;max-width:560px;padding:8px 0}
824
- .fm-setting-row{display:flex;align-items:center;gap:10px;font-size:14px;cursor:pointer;user-select:none}
825
- .fm-setting-row input[type="checkbox"]{width:16px;height:16px;cursor:pointer}
826
- .fm-setting-row input[type="range"]{flex:1;max-width:240px;cursor:pointer}
897
+ .fm-plugin-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
898
+ .fm-plugin-card:hover{border-color:var(--dsw-alias-label-dimmed)}
899
+ .fm-plugin-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
900
+ .fm-plugin-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
901
+ .fm-plugin-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
902
+ .fm-plugin-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
903
+ .fm-plugin-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
904
+ .fm-plugin-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
905
+ .fm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
906
+ .fm-plugin-card-chevron-open{transform:rotate(180deg)}
907
+ .fm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}
908
+ .fm-plugin-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
909
+ .fm-plugin-field+.fm-plugin-field{border-top:1px solid var(--dsw-alias-border-l2)}
910
+ .fm-plugin-field-head{align-items:center;gap:8px;display:flex}
911
+ .fm-plugin-field-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
912
+ .fm-plugin-field-value{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
913
+ .fm-plugin-field-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
914
+ .fm-plugin-check{align-items:center;gap:8px;display:flex;cursor:pointer;user-select:none}
915
+ .fm-plugin-check input[type="checkbox"]{width:16px;height:16px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#4176e6)}
916
+ .fm-plugin-check-label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}
917
+ .fm-plugin-range{width:100%;max-width:280px;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#4176e6)}
827
918
  `;
828
919
  //#endregion
829
920
  //#region src/client/locales.ts
@@ -834,10 +925,17 @@ window.__ModuleLoader__.load({
834
925
  "exit": "退出专注 (Esc)",
835
926
  "backToLatest": "回到最新",
836
927
  "settings.label": "专注模式",
928
+ "settings.description": "全屏阅读遮罩的偏好设置",
929
+ "settings.expand": "展开",
930
+ "settings.collapse": "收起",
837
931
  "settings.navbar": "显示右侧导航条",
932
+ "settings.navbar.hint": "在右侧显示用户消息导航圆点",
838
933
  "settings.scrollPreserve": "打开时保留聊天位置",
934
+ "settings.scrollPreserve.hint": "打开时定位到当前阅读位置;关闭则定位到最新",
839
935
  "settings.autoFocus": "回复结束后自动进入专注模式",
936
+ "settings.autoFocus.hint": "回复正常完成后自动打开;AI 提问 / 审批等待时弹出提醒",
840
937
  "settings.width": "文字区宽度",
938
+ "settings.width.hint": "专注模式阅读列宽度(480–1200px)",
841
939
  "empty": "暂无打开的会话",
842
940
  "loading": "正在读取会话…",
843
941
  "running": "· 正在工作 …",
@@ -867,10 +965,17 @@ window.__ModuleLoader__.load({
867
965
  "exit": "Exit focus (Esc)",
868
966
  "backToLatest": "Back to latest",
869
967
  "settings.label": "Focus Mode",
968
+ "settings.description": "Preferences for the full-screen reading overlay",
969
+ "settings.expand": "Expand",
970
+ "settings.collapse": "Collapse",
870
971
  "settings.navbar": "Show right-side navbar",
972
+ "settings.navbar.hint": "Show navigation dots for user messages on the right",
871
973
  "settings.scrollPreserve": "Preserve chat position on open",
974
+ "settings.scrollPreserve.hint": "Open at your current reading position; off opens at the latest",
872
975
  "settings.autoFocus": "Auto-enter focus mode when a reply completes",
976
+ "settings.autoFocus.hint": "Auto-open after a normal completion; toast while AI waits for you",
873
977
  "settings.width": "Text width",
978
+ "settings.width.hint": "Reading column width (480–1200px)",
874
979
  "empty": "No open session",
875
980
  "loading": "Reading session…",
876
981
  "running": "· Working …",
@@ -899,7 +1004,8 @@ window.__ModuleLoader__.load({
899
1004
  /**
900
1005
  * Focus Mode, browser half: a full-screen reading overlay in `shell.overlay`
901
1006
  * (additive, never replaces shipped UI), a "专注" button in the session header
902
- * action row, and a settings section. Rendering reuses the official
1007
+ * action row, and a collapsible plugin card in the Plugins settings tab
1008
+ * (`settings.plugin.item`). Rendering reuses the official
903
1009
  * `@deepseek-ai/dsh-client-ui-primitives` Markdown/MessageText/Tooltip/Button
904
1010
  * components and the conversation service's image resolver.
905
1011
  */
@@ -936,12 +1042,14 @@ window.__ModuleLoader__.load({
936
1042
  let currentId = void 0;
937
1043
  let unsubSession = null;
938
1044
  let prevRunning = false;
1045
+ let pendingSettle = false;
939
1046
  const watch = (id) => {
940
1047
  if (unsubSession) {
941
1048
  unsubSession();
942
1049
  unsubSession = null;
943
1050
  }
944
1051
  prevRunning = false;
1052
+ pendingSettle = false;
945
1053
  if (id == null) return;
946
1054
  const binding = sessions.binding(id);
947
1055
  if (!binding) return;
@@ -950,12 +1058,17 @@ window.__ModuleLoader__.load({
950
1058
  const snap = face.getSnapshot();
951
1059
  if (!snap) return;
952
1060
  const running = !!snap.running;
953
- if (prevRunning && !running) {
1061
+ if (prevRunning && !running) pendingSettle = true;
1062
+ if (running) pendingSettle = false;
1063
+ if (pendingSettle && !running) {
954
1064
  const outcome = detectSettledCompletion(snap);
955
- if (outcome.completed && prefsStore.get().autoFocus) if (focusStore.get()) focusStore.notifyDone();
956
- else {
957
- focusStore.setAutoAnchor(outcome.anchorSeq);
958
- focusStore.set(true);
1065
+ if (outcome.settled) {
1066
+ pendingSettle = false;
1067
+ if (outcome.completed && prefsStore.get().autoFocus) if (focusStore.get()) focusStore.notifyDone();
1068
+ else {
1069
+ focusStore.setAutoAnchor(outcome.anchorSeq);
1070
+ focusStore.set(true);
1071
+ }
959
1072
  }
960
1073
  }
961
1074
  prevRunning = running;
@@ -996,12 +1109,12 @@ window.__ModuleLoader__.load({
996
1109
  order: 1e3,
997
1110
  label: () => t("toggle")
998
1111
  }, () => (0, react.createElement)(FocusToggle, { t })));
999
- ctx.slots.inject("settings.section", () => ctx.slots.register({
1000
- name: "settings.section",
1001
- id: "focus-mode-settings",
1002
- order: 100,
1003
- label: () => t("settings.label")
1004
- }, () => (0, react.createElement)(FocusSettings, { t })));
1112
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
1113
+ name: "settings.plugin.item",
1114
+ key: "dsh-focus-overlay",
1115
+ id: "dsh-focus-overlay",
1116
+ order: 1e3
1117
+ }, () => (0, react.createElement)(FocusSettingsCard, { t })));
1005
1118
  }
1006
1119
  };
1007
1120
  return module.exports;
package/lib/index.mjs CHANGED
@@ -1,4 +1,24 @@
1
+ import z from "@deepseek-ai/schemastery";
1
2
  //#region src/index.mjs
2
- function apply() {}
3
+ /** Settings namespace owned by this plugin (must match /^[a-z][a-z0-9-]*$/). */
4
+ const NAMESPACE = "dsh-focus-overlay";
5
+ /** Durable settings schema; defaults mirror the browser localStorage DEFAULTS. */
6
+ const FocusSettingsSchema = z.object({
7
+ navbar: z.boolean().default(true),
8
+ scroll: z.union(["preserve", "bottom"]).default("preserve"),
9
+ width: z.number().default(760),
10
+ autoFocus: z.boolean().default(false)
11
+ });
12
+ function apply(ctx) {
13
+ ctx.inject(["settings"], (settingsCtx) => {
14
+ const settings = settingsCtx.settings;
15
+ if (!settings || typeof settings.register !== "function") return;
16
+ try {
17
+ settings.register(NAMESPACE, FocusSettingsSchema);
18
+ } catch (error) {
19
+ console.warn("[dsh-focus-overlay] settings namespace registration failed:", error);
20
+ }
21
+ });
22
+ }
3
23
  //#endregion
4
- export { apply as default };
24
+ export { FocusSettingsSchema, NAMESPACE, apply as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-focus-overlay",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Focus Mode for the dsh web GUI: a full-screen reading overlay that hides chrome and folds the AI tool-call flow into summaries, reusing the official Markdown / image primitives.",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",
@@ -55,5 +55,8 @@
55
55
  "type": "git",
56
56
  "url": "https://github.com/boogoo619/dsh-focus-overlay.git"
57
57
  },
58
- "license": "MIT"
58
+ "license": "MIT",
59
+ "dependencies": {
60
+ "@deepseek-ai/schemastery": "^3.18.1"
61
+ }
59
62
  }