dsh-flyout-sidebar 0.1.8 → 0.1.10

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.
Files changed (3) hide show
  1. package/dist/client.js +243 -133
  2. package/dist/index.js +145 -38
  3. package/package.json +2 -2
package/dist/client.js CHANGED
@@ -65,11 +65,12 @@
65
65
  /* 面板关闭时:corner 触发按钮滑回右上角(占 right 12~48px 区间),左上角
66
66
  panel 不动,日志按钮让位留白 —— 与 better-sidebar 折叠态给右上角按钮组
67
67
  让位(right 10→70px)的思路一致,避免两块按钮挤在一起(见下方 64px 规则)。 */
68
- /* 面板打开时:以面板宽度精确避让右上角按钮,并在面板左侧留 16px 间距,日志
69
- 按钮不紧贴面板边框。用 !important 压过宿主/其它插件对同一 header 的
70
- padding 规则(如 better-sidebar 折叠态强加的 78px)。 */
68
+ /* 面板打开:header 已被 AppFrame grid 推挤到面板左缘(components.tsx 追加
69
+ 轨道),这里只需再留固定 16px 间距,日志按钮与面板保持恒定距离 —— 不能
70
+ 再按面板宽让位一次,否则推挤 + padding 叠加,日志按钮会被推出去两倍
71
+ 面板宽(距面板过远)。用 !important 压过宿主/其它插件的同规则。 */
71
72
  body.dsh-flyout-sidebar-open header:has([data-slot="conversation.session.header.utilities"]) {
72
- padding-right: calc(var(--dsh-flyout-sidebar-width, 0px) + 16px) !important;
73
+ padding-right: 16px !important;
73
74
  transition: padding-right var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease);
74
75
  }
75
76
  /* 面板收起:日志按钮让位到 64px(= corner 按钮 48px 左缘 + 16px 间距)。
@@ -357,17 +358,9 @@ body[data-ds-dark-theme] .tok-function, body[data-ds-dark-theme] .tok-decorator
357
358
  body[data-ds-dark-theme] .tok-class, body[data-ds-dark-theme] .tok-builtin, body[data-ds-dark-theme] .tok-tag, body[data-ds-dark-theme] .tok-key { color: #74c0fc; }
358
359
  body[data-ds-dark-theme] .tok-property { color: #ced4da; }
359
360
 
360
- /* Settings 卡片(Settings → Plugins → Plugin configuration)— 跟随 DSH PluginCard 风格 */
361
- .fs-settings-card { background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; transition: border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
362
- .fs-settings-card:hover { border-color: var(--dsw-alias-label-dimmed); }
363
- .fs-settings-open, .fs-settings-open:hover { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-label-dimmed); }
364
- .fs-settings-head { width: 100%; appearance: none; border: 0; background: none; font: inherit; color: inherit; text-align: left; cursor: pointer; display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-radius: 12px; }
365
- .fs-settings-headtext { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
366
- .fs-settings-name { font-size: 15px; font-weight: 600; line-height: 1.4; color: var(--dsw-alias-label-primary); }
367
- .fs-settings-desc { font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
368
- .fs-settings-chevron { flex: none; color: var(--dsw-alias-label-tertiary); transition: transform var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
369
- .fs-settings-open .fs-settings-chevron { transform: rotate(180deg); }
370
- .fs-settings-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 4px 0 12px; display: flex; flex-direction: column; gap: 0; }
361
+ /* 插件配置页(插件页 → 本插件的行 → 配置):行/标题/描述由插件管理页绘制,
362
+ 这里只负责表单本体与保存控件,跟随 DSH 设计令牌。 */
363
+ .fs-settings-form { display: flex; flex-direction: column; gap: 0; }
371
364
  .fs-settings-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; }
372
365
  .fs-settings-label { flex: 1; min-width: 0; font-size: 14px; color: var(--dsw-alias-label-primary); }
373
366
  .fs-settings-input {
@@ -379,6 +372,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
379
372
  }
380
373
  .fs-settings-input:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
381
374
  .fs-settings-input:disabled { opacity: .5; cursor: default; }
375
+ .fs-settings-input[data-invalid="true"] { border-color: var(--dsw-alias-accent-error, #e5484d); }
382
376
  .fs-settings-toggle {
383
377
  position: relative; width: 40px; height: 22px; border-radius: 11px;
384
378
  border: none; cursor: pointer; padding: 0; flex: none;
@@ -392,6 +386,18 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
392
386
  background: #fff; transition: transform var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out);
393
387
  }
394
388
  .fs-settings-toggle[data-on="true"]::after { transform: translateX(18px); }
389
+ .fs-settings-actions { display: flex; align-items: center; gap: 10px; padding: 12px 0 4px; }
390
+ .fs-settings-save {
391
+ appearance: none; border: 1px solid transparent; border-radius: 8px; cursor: pointer;
392
+ padding: 5px 14px; font: inherit; font-size: 13px; font-weight: 500;
393
+ background: var(--dsw-alias-accent-primary, #2a7fbf); color: #fff;
394
+ transition: opacity var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out);
395
+ }
396
+ .fs-settings-save:disabled { opacity: .45; cursor: default; }
397
+ .fs-settings-status, .fs-settings-note {
398
+ font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary);
399
+ }
400
+ .fs-settings-note { margin: 4px 0 8px; }
395
401
  `;
396
402
  /** 注入样式(幂等:内容不一致时替换,使热重载后新样式必然生效) */
397
403
  function insertStyles() {
@@ -553,7 +559,13 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
553
559
  settingsAutoRefresh: "打开面板时自动刷新",
554
560
  settingsMinWidth: "面板最小宽度(%)",
555
561
  settingsDefaultOpen: "页面加载后默认展开",
556
- settingsFontSize: "内容区字号(px)"
562
+ settingsFontSize: "内容区字号(px)",
563
+ settingsSave: "保存",
564
+ settingsSaving: "保存中…",
565
+ settingsSaveFailed: "宿主未接受这些值,已保留供你修改。",
566
+ settingsReadonly: "本部署的设置为只读。",
567
+ settingsUnavailable: "该插件当前未加载,暂时无法配置。",
568
+ settingsInvalidNumber: "请填范围内的数字;留空表示恢复默认。"
557
569
  };
558
570
  /** @type {Record<string, string>} */
559
571
  const EN = {
@@ -629,7 +641,13 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
629
641
  settingsAutoRefresh: "Auto-refresh on panel open",
630
642
  settingsMinWidth: "Min panel width (%)",
631
643
  settingsDefaultOpen: "Open by default on page load",
632
- settingsFontSize: "Content font size (px)"
644
+ settingsFontSize: "Content font size (px)",
645
+ settingsSave: "Save",
646
+ settingsSaving: "Saving…",
647
+ settingsSaveFailed: "The deployment did not accept these values; they were left for you to correct.",
648
+ settingsReadonly: "This deployment stores settings read-only.",
649
+ settingsUnavailable: "This plugin is not loaded, so it cannot be configured right now.",
650
+ settingsInvalidNumber: "Enter a number within range, or leave blank to use the default."
633
651
  };
634
652
  /** @type {Record<string, Record<string, string>>} */
635
653
  const DICTS = {
@@ -877,11 +895,11 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
877
895
  * 从宿主拉取插件配置(config 已由 cordis 校验填充默认值)并通知订阅者。
878
896
  * 配置保存在宿主侧,页面内不持久化;面板打开即用最新值。
879
897
  *
880
- * syncDefaultOpen=true(启动首次加载、settings scope 配置变更回调)时,
881
- * 若本次拉取确实改动了 defaultOpen 字段,则实时同步边栏开合 —— 用户在
882
- * 设置卡片切换「默认展开」开关后边栏立即跟随展开/收起。面板摊开时例行
883
- * 拉取(刷新 minPanelWidth 等设置)不传此 flag:只刷新配置,绝不回写
884
- * 开合状态,否则刚点开的边栏会被 defaultOpen 当场压回(点击无反应)。
898
+ * syncDefaultOpen=true(启动首次加载)时,若本次拉取确实改动了 defaultOpen
899
+ * 字段,则实时同步边栏开合 —— 用户改完「默认展开」设置后重新加载页面,边栏
900
+ * 立即按新偏好展开/收起。面板摊开时例行拉取(刷新 minPanelWidth 等设置)不传
901
+ * 此 flag:只刷新配置,绝不回写开合状态,否则刚点开的边栏会被 defaultOpen
902
+ * 当场压回(点击无反应)。
885
903
  */
886
904
  load(opts) {
887
905
  const syncDefaultOpen = !!(opts && opts.syncDefaultOpen);
@@ -2997,95 +3015,223 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2997
3015
  onClick: () => store.toggle()
2998
3016
  }, /* @__PURE__ */ h(PanelIcon, { size: 18 }));
2999
3017
  }
3018
+ //#endregion
3019
+ //#region src/client/config.tsx
3020
+ /**
3021
+ * Client 侧:插件配置页(插件页 → 本插件的详情页 → 配置表单)。
3022
+ *
3023
+ * 新版 DSH 客户端不再为插件的 volatile 配置自动生成设置卡片:配置页必须由插件
3024
+ * 自己注册。bundle 级配置注册进 `plugins.bundle.config`(key = 包名),宿主插件
3025
+ * 管理页会把它直接渲染在本插件的详情页上、位于描述与行列表之间(仅 `page`
3026
+ * 视图)。宿主侧不需要任何注册 —— settings 服务直接投影 Loader 条目里带
3027
+ * `.volatile()` 的字段,以条目 id(= 行 id)作为 namespace,配置值经
3028
+ * `ctx.configForms.get(条目 id)` 读写(插件管理页把它作为 owner props 的 `form`
3029
+ * 传下来,注册方通过注入的表单引用拿实时快照)。
3030
+ *
3031
+ * 表单是「一页一存」:控件只改本地草稿,保存时把所有改动合成一次带 revision
3032
+ * fence 的 mutate —— 与官方伴生包的 SettingsFormModel 语义一致,这样一次保存
3033
+ * 只产生一次宿主写入,用户也能在保存前看到自己要写什么。
3034
+ */
3035
+ /** bundle config slot 的注册 key:包名(dsh-flyout-sidebar) */
3036
+ const BUNDLE_CONFIG_KEY = "dsh-flyout-sidebar";
3037
+ /** 宿主 settings 的 namespace:profile 条目 id,也就是插件行的 row id */
3038
+ const SETTINGS_NAMESPACE = "flyout-sidebar";
3039
+ const FIELDS = [
3040
+ {
3041
+ field: "autoRefresh",
3042
+ labelKey: "settingsAutoRefresh",
3043
+ kind: "toggle",
3044
+ min: 0,
3045
+ max: 0
3046
+ },
3047
+ {
3048
+ field: "defaultOpen",
3049
+ labelKey: "settingsDefaultOpen",
3050
+ kind: "toggle",
3051
+ min: 0,
3052
+ max: 0
3053
+ },
3054
+ {
3055
+ field: "minPanelWidth",
3056
+ labelKey: "settingsMinWidth",
3057
+ kind: "number",
3058
+ min: 15,
3059
+ max: 60
3060
+ },
3061
+ {
3062
+ field: "contentFontSize",
3063
+ labelKey: "settingsFontSize",
3064
+ kind: "number",
3065
+ min: 11,
3066
+ max: 20
3067
+ }
3068
+ ];
3069
+ /** 宿主值 → 草稿文本:开关用 'true'/'false',数字无值时留空(= 未覆盖) */
3070
+ function seedText(spec, stored) {
3071
+ if (spec.kind === "toggle") return stored === true ? "true" : "false";
3072
+ return typeof stored === "number" && Number.isFinite(stored) ? String(stored) : "";
3073
+ }
3074
+ function seed(values) {
3075
+ const out = {};
3076
+ for (const spec of FIELDS) out[spec.field] = seedText(spec, values?.[spec.field]);
3077
+ return out;
3078
+ }
3079
+ /** 草稿文本 → 数字;空文本表示恢复默认,越界或非数字视为无效(阻止保存) */
3080
+ function parseNumber(spec, text) {
3081
+ const trimmed = text.trim();
3082
+ if (trimmed === "") return {
3083
+ ok: true,
3084
+ value: null
3085
+ };
3086
+ const n = Number(trimmed);
3087
+ if (!Number.isFinite(n) || n < spec.min || n > spec.max) return {
3088
+ ok: false,
3089
+ value: null
3090
+ };
3091
+ return {
3092
+ ok: true,
3093
+ value: n
3094
+ };
3095
+ }
3096
+ /** 把有改动的字段合成一次写入的操作序列;无效字段不产生操作(保存已被阻止) */
3097
+ function planOps(draft, remote) {
3098
+ const ops = [];
3099
+ for (const spec of FIELDS) {
3100
+ const text = draft[spec.field] ?? "";
3101
+ if (text === (remote[spec.field] ?? "")) continue;
3102
+ if (spec.kind === "toggle") {
3103
+ ops.push({
3104
+ op: "set",
3105
+ path: [spec.field],
3106
+ value: text === "true"
3107
+ });
3108
+ continue;
3109
+ }
3110
+ const parsed = parseNumber(spec, text);
3111
+ if (!parsed.ok) continue;
3112
+ ops.push(parsed.value === null ? {
3113
+ op: "unset",
3114
+ path: [spec.field]
3115
+ } : {
3116
+ op: "set",
3117
+ path: [spec.field],
3118
+ value: parsed.value
3119
+ });
3120
+ }
3121
+ return ops;
3122
+ }
3000
3123
  function ToggleRow(props) {
3001
- return /* @__PURE__ */ h("div", { className: "fs-settings-row" }, /* @__PURE__ */ h("span", { className: "fs-settings-label" }, props.label), /* @__PURE__ */ h("button", {
3124
+ return /* @__PURE__ */ h(Fragment, null, /* @__PURE__ */ h("span", { className: "fs-settings-label" }, props.label), /* @__PURE__ */ h("button", {
3002
3125
  type: "button",
3003
3126
  className: "fs-settings-toggle",
3004
3127
  "data-on": props.value ? "true" : "false",
3005
3128
  disabled: props.disabled,
3006
3129
  "aria-pressed": props.value,
3130
+ "aria-label": props.label,
3007
3131
  onClick: () => {
3008
3132
  if (!props.disabled) props.onToggle(!props.value);
3009
3133
  }
3010
3134
  }));
3011
3135
  }
3012
3136
  function NumberRow(props) {
3013
- return /* @__PURE__ */ h("div", { className: "fs-settings-row" }, /* @__PURE__ */ h("span", { className: "fs-settings-label" }, props.label), /* @__PURE__ */ h("input", {
3137
+ return /* @__PURE__ */ h(Fragment, null, /* @__PURE__ */ h("span", { className: "fs-settings-label" }, props.label), /* @__PURE__ */ h("input", {
3014
3138
  type: "number",
3015
3139
  className: "fs-settings-input",
3016
- value: props.value,
3140
+ value: props.text,
3017
3141
  min: props.min,
3018
3142
  max: props.max,
3019
3143
  disabled: props.disabled,
3020
- onChange: (e) => {
3021
- const v = Number(e.currentTarget.value);
3022
- if (Number.isFinite(v)) props.onChange(v);
3023
- }
3144
+ "aria-label": props.label,
3145
+ "aria-invalid": props.invalid,
3146
+ "data-invalid": props.invalid ? "true" : "false",
3147
+ onChange: (e) => props.onEdit(e.currentTarget.value)
3024
3148
  }));
3025
3149
  }
3026
- function SettingsCard(props) {
3027
- const [open, setOpen] = React.useState(false);
3150
+ /**
3151
+ * 订阅宿主表单的实时快照:写入被接受后宿主镜像会折叠出新快照,本控件据此
3152
+ * 重新播种草稿(插件管理页是否跟着重渲染与本页无关)。
3153
+ */
3154
+ function useConfigSnapshot(form, fallback) {
3155
+ const [snap, setSnap] = React.useState(() => form ? form.getSnapshot() : fallback);
3156
+ React.useEffect(() => {
3157
+ if (!form) return;
3158
+ setSnap(form.getSnapshot());
3159
+ return form.subscribe(() => setSnap(form.getSnapshot()));
3160
+ }, [form]);
3161
+ return form ? snap : fallback;
3162
+ }
3163
+ function FlyoutConfigPage(props) {
3028
3164
  useLang();
3029
- const snap = typeof props.useSettingsSnapshot === "function" ? props.useSettingsSnapshot((s) => s) : void 0;
3030
- if (snap === void 0 || snap.status === "unavailable") return null;
3031
- const disabled = snap.status !== "ready" || !snap.writable;
3032
- const v = snap.value ?? {};
3033
- return /* @__PURE__ */ h("li", { className: "fs-settings-card" + (open ? " fs-open" : "") }, /* @__PURE__ */ h("button", {
3034
- type: "button",
3035
- className: "fs-settings-head",
3036
- "aria-expanded": open,
3037
- onClick: () => {
3038
- setOpen(!open);
3039
- }
3040
- }, /* @__PURE__ */ h("span", { className: "fs-settings-headtext" }, /* @__PURE__ */ h("span", { className: "fs-settings-name" }, t("settingsTitle")), /* @__PURE__ */ h("span", { className: "fs-settings-desc" }, t("settingsDesc"))), /* @__PURE__ */ h("svg", {
3041
- className: "fs-settings-chevron",
3042
- width: "14",
3043
- height: "14",
3044
- viewBox: "0 0 14 14",
3045
- fill: "none"
3046
- }, /* @__PURE__ */ h("path", {
3047
- d: "M3 5L7 9L11 5",
3048
- stroke: "currentColor",
3049
- strokeWidth: "1.5",
3050
- strokeLinecap: "round",
3051
- strokeLinejoin: "round"
3052
- }))), open ? /* @__PURE__ */ h("div", { className: "fs-settings-body" }, /* @__PURE__ */ h(ToggleRow, {
3053
- label: t("settingsAutoRefresh"),
3054
- value: !!v.autoRefresh,
3055
- disabled,
3056
- onToggle: (val) => {
3057
- props.setField?.("autoRefresh", val);
3058
- }
3059
- }), /* @__PURE__ */ h(ToggleRow, {
3060
- label: t("settingsDefaultOpen"),
3061
- value: !!v.defaultOpen,
3062
- disabled,
3063
- onToggle: (val) => {
3064
- props.setField?.("defaultOpen", val);
3065
- }
3066
- }), /* @__PURE__ */ h(NumberRow, {
3067
- label: t("settingsMinWidth"),
3068
- value: v.minPanelWidth ?? 20,
3069
- min: 15,
3070
- max: 60,
3165
+ const form = props.configForm;
3166
+ const snap = useConfigSnapshot(form, props.form?.state);
3167
+ const values = snap?.value;
3168
+ const remote = React.useMemo(() => seed(values), [snap]);
3169
+ const [staged, setStaged] = React.useState(() => ({
3170
+ revision: snap?.revision,
3171
+ text: {}
3172
+ }));
3173
+ const [saving, setSaving] = React.useState(false);
3174
+ const [failed, setFailed] = React.useState(false);
3175
+ const edits = staged.revision === snap?.revision ? staged.text : {};
3176
+ if (props.view === "summary") return /* @__PURE__ */ h(Fragment, null, t("settingsDesc"));
3177
+ if (snap === void 0 || snap.status === "unavailable") return /* @__PURE__ */ h("p", { className: "fs-settings-note" }, t("settingsUnavailable"));
3178
+ const draft = {
3179
+ ...remote,
3180
+ ...edits
3181
+ };
3182
+ const textOf = (field) => draft[field] ?? "";
3183
+ const dirty = FIELDS.some((spec) => textOf(spec.field) !== (remote[spec.field] ?? ""));
3184
+ const invalid = FIELDS.some((spec) => spec.kind === "number" && !parseNumber(spec, textOf(spec.field)).ok);
3185
+ const disabled = snap.status !== "ready" || snap.writable === false || saving;
3186
+ const mutate = form?.mutate ?? props.form?.mutate;
3187
+ const stage = (field, text) => {
3188
+ setFailed(false);
3189
+ setStaged({
3190
+ revision: snap.revision,
3191
+ text: {
3192
+ ...edits,
3193
+ [field]: text
3194
+ }
3195
+ });
3196
+ };
3197
+ const save = () => {
3198
+ if (!mutate || !dirty || invalid || saving) return;
3199
+ setSaving(true);
3200
+ setFailed(false);
3201
+ mutate(planOps(draft, remote), snap.revision).then((ok) => {
3202
+ setSaving(false);
3203
+ if (ok) return;
3204
+ setFailed(true);
3205
+ }).catch(() => {
3206
+ setSaving(false);
3207
+ setFailed(true);
3208
+ });
3209
+ };
3210
+ return /* @__PURE__ */ h("div", { className: "fs-settings-form" }, snap.writable === false ? /* @__PURE__ */ h("p", { className: "fs-settings-note" }, t("settingsReadonly")) : null, FIELDS.map((spec) => /* @__PURE__ */ h("div", {
3211
+ className: "fs-settings-row",
3212
+ key: spec.field
3213
+ }, spec.kind === "toggle" ? /* @__PURE__ */ h(ToggleRow, {
3214
+ label: t(spec.labelKey),
3215
+ value: textOf(spec.field) === "true",
3071
3216
  disabled,
3072
- onChange: (val) => {
3073
- props.setField?.("minPanelWidth", val);
3074
- }
3075
- }), /* @__PURE__ */ h(NumberRow, {
3076
- label: t("settingsFontSize"),
3077
- value: v.contentFontSize ?? 13,
3078
- min: 11,
3079
- max: 20,
3217
+ onToggle: (next) => stage(spec.field, next ? "true" : "false")
3218
+ }) : /* @__PURE__ */ h(NumberRow, {
3219
+ label: t(spec.labelKey),
3220
+ text: textOf(spec.field),
3221
+ min: spec.min,
3222
+ max: spec.max,
3223
+ invalid: !parseNumber(spec, textOf(spec.field)).ok,
3080
3224
  disabled,
3081
- onChange: (val) => {
3082
- props.setField?.("contentFontSize", val);
3083
- }
3084
- })) : null);
3225
+ onEdit: (text) => stage(spec.field, text)
3226
+ }))), /* @__PURE__ */ h("div", { className: "fs-settings-actions" }, /* @__PURE__ */ h("button", {
3227
+ type: "button",
3228
+ className: "fs-settings-save",
3229
+ disabled: disabled || !dirty || invalid,
3230
+ onClick: save
3231
+ }, saving ? t("settingsSaving") : t("settingsSave")), invalid ? /* @__PURE__ */ h("span", { className: "fs-settings-status" }, t("settingsInvalidNumber")) : null, !invalid && failed ? /* @__PURE__ */ h("span", { className: "fs-settings-status" }, t("settingsSaveFailed")) : null));
3085
3232
  }
3086
3233
  //#endregion
3087
3234
  //#region src/client/index.tsx
3088
- const SETTINGS_NAMESPACE = "dsh-flyout-sidebar";
3089
3235
  window.__ModuleLoader__.load({
3090
3236
  id: "dsh-flyout-sidebar",
3091
3237
  factory: (require) => {
@@ -3109,54 +3255,18 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
3109
3255
  order: 50,
3110
3256
  label: "Artifacts Panel"
3111
3257
  }, () => /* @__PURE__ */ h(ArtifactsPanel, null)));
3112
- settingsStore.load({ syncDefaultOpen: true });
3113
- ctx.inject(["settingsScope"], (sctx) => {
3114
- const binder = sctx.get("settingsScope");
3115
- if (!binder) return;
3116
- const scope = binder.bind({ namespace: SETTINGS_NAMESPACE });
3117
- const scopedSlots = sctx.get("slots");
3118
- if (!scopedSlots) return;
3119
- let localSnap = scope.getSnapshot();
3120
- const localListeners = /* @__PURE__ */ new Set();
3121
- const publishLocal = (next) => {
3122
- if (next === localSnap) return;
3123
- localSnap = next;
3124
- for (const fn of localListeners) try {
3125
- fn();
3126
- } catch {}
3127
- };
3128
- sctx.effect(() => scope.subscribe(() => {
3129
- publishLocal(scope.getSnapshot());
3130
- settingsStore.load({ syncDefaultOpen: true });
3131
- }), "flyout: settings scope");
3132
- const useSettingsSnapshot = (selector) => React.useSyncExternalStore((cb) => {
3133
- localListeners.add(cb);
3134
- return () => {
3135
- localListeners.delete(cb);
3136
- };
3137
- }, () => selector(localSnap));
3138
- const setField = (field, value) => {
3139
- const cur = localSnap;
3140
- const nextValue = cur.value && typeof cur.value === "object" ? {
3141
- ...cur.value,
3142
- [field]: value
3143
- } : { [field]: value };
3144
- publishLocal({
3145
- ...cur,
3146
- value: nextValue
3147
- });
3148
- scope.set(field, value);
3149
- };
3150
- scopedSlots.inject("settings.plugin.item", () => scopedSlots.register({
3151
- name: "settings.plugin.item",
3152
- key: SETTINGS_NAMESPACE,
3153
- locale: SETTINGS_NAMESPACE,
3154
- inject: () => ({
3155
- useSettingsSnapshot,
3156
- setField
3157
- })
3158
- }, (props) => h(SettingsCard, props)));
3258
+ ctx.inject(["configForms"], (sub) => {
3259
+ const configForms = sub.get("configForms");
3260
+ if (!configForms) return;
3261
+ ctx.effect(() => configForms.whileServed([SETTINGS_NAMESPACE], () => slots.inject("plugins.bundle.config", () => slots.register({
3262
+ name: "plugins.bundle.config",
3263
+ key: BUNDLE_CONFIG_KEY,
3264
+ order: 40,
3265
+ label: () => t("settingsTitle"),
3266
+ inject: () => ({ configForm: configForms.get(SETTINGS_NAMESPACE) })
3267
+ }, FlyoutConfigPage))), "dsh-flyout-sidebar: config page");
3159
3268
  });
3269
+ settingsStore.load({ syncDefaultOpen: true });
3160
3270
  }
3161
3271
  };
3162
3272
  }
package/dist/index.js CHANGED
@@ -1022,7 +1022,7 @@ var highlight_default = "/**\n * 共享:零依赖语法高亮器(无模板
1022
1022
  var markdown_default = "/**\n * 共享:极简 Markdown → HTML 渲染器。\n *\n * 可移植性约束见 ext.ts 顶部说明:本文件经 `?raw` 原样内联进独立弹出页的\n * 经典 <script>,只能使用 JSDoc 标注类型。围栏代码块通过 shared/highlight\n * 的 highlightCode 高亮。\n */\nimport { highlightCode } from './highlight.js'\n\n/** @param {string} s @returns {string} */\nfunction mdEscape(s) {\n return String(s).replace(/[\\u0000\\u0001]/g, '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n\n/**\n * URL 白名单:只放行 http(s)、站内相对路径、页内锚点与 data:image/*,\n * 其余(javascript:、vbscript: 及实体编码变体等)一律替换为 '#',防止\n * 预览渲染出来的链接/图片在点击时执行脚本。\n * @param {string} u @returns {string}\n */\nfunction mdSafeUrl(u) {\n var s = String(u || '').replace(/[\\s\\u0000-\\u001f]/g, '')\n if (/^https?:\\/\\//i.test(s)) return s\n if (/^\\/(?!\\/)/.test(s) || /^\\.{1,2}\\//.test(s) || s.charAt(0) === '#') return s\n if (/^data:image\\/(?:png|gif|jpeg|webp|bmp|avif);/i.test(s)) return s\n return '#'\n}\n\n/** @param {string} s @returns {string} */\nfunction mdInline(s) {\n // code span 先摘出为占位符再跑后续规则:反引号内的 `[x](url)`、`**b**`\n // 应原样输出,不被行内链接/强调规则二次渲染(占位符字符已在 mdEscape 剥除)。\n /** @type {string[]} */\n const codes = []\n s = s.replace(/`([^`]+)`/g, (m, c) => {\n codes.push('<code>' + c + '</code>')\n return '\\u0000' + (codes.length - 1) + '\\u0000'\n })\n s = s.replace(/!\\[([^\\]]*)\\]\\(([^)\\s]+)\\)/g, (m, alt, u) => '<img alt=\"' + alt + '\" src=\"' + mdSafeUrl(u) + '\">')\n s = s.replace(/\\[([^\\]]+)\\]\\(([^)\\s]+)\\)/g, (m, text, u) => '<a href=\"' + mdSafeUrl(u) + '\" target=\"_blank\" rel=\"noopener noreferrer\">' + text + '</a>')\n s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n s = s.replace(/\\*([^*\\n]+)\\*/g, '<em>$1</em>')\n s = s.replace(/\\u0000(\\d+)\\u0000/g, (m, i) => codes[Number(i)] ?? '')\n return s\n}\n\n/**\n * GFM 管道表格辅助:按未转义的 | 切分单元格(行内 \\| 还原为字面量竖线),\n * 并去掉首尾管道产生的空单元格。\n * @param {string} line @returns {string[]}\n */\nfunction mdSplitCells(line) {\n const parts = String(line).replace(/\\\\\\|/g, '\\u0002').split('|')\n for (let i = 0; i < parts.length; i += 1) {\n parts[i] = (parts[i] || '').replace(/\\u0002/g, '|').trim()\n }\n return parts\n}\n\n/** @param {string[]} cells @returns {string[]} */\nfunction mdTrimEdgeCells(cells) {\n if (cells.length && cells[0] === '') cells.shift()\n if (cells.length && cells[cells.length - 1] === '') cells.pop()\n return cells\n}\n\n/** @param {string[]} cells @returns {boolean} 是否为 GFM 分隔行(:--- / :---: / ---: 形态) */\nfunction mdIsDelimiterRow(cells) {\n return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c))\n}\n\n/**\n * @param {string} cell @returns {string} 'center' | 'right' | ''(left 与默认一致,不输出)\n */\nfunction mdCellAlign(cell) {\n if (/^:-+:$/.test(cell)) return 'center'\n if (/^:-+$/.test(cell)) return 'left'\n if (/^-+:$/.test(cell)) return 'right'\n return ''\n}\n\n/** @param {string} a @returns {string} 单元格对齐的内联样式属性 */\nfunction mdAlignAttr(a) {\n return a === 'center' || a === 'right' ? ' style=\"text-align:' + a + '\"' : ''\n}\n\n/** @param {string} src @returns {string} HTML */\nexport function mdToHtml(src) {\n const lines = String(src || '').replace(/\\r\\n/g, '\\n').split('\\n')\n /** @type {string[]} */\n const out = []\n let i = 0\n while (i < lines.length) {\n const line = /** @type {string} */ (lines[i])\n if (/^\\s*```/.test(line)) {\n const fence = /^\\s*```([\\w+-]*)/.exec(line)\n const langHint = fence ? fence[1] || '' : ''\n /** @type {string[]} */\n const buf = []\n i += 1\n while (i < lines.length && !/^\\s*```/.test(/** @type {string} */ (lines[i]))) {\n buf.push(/** @type {string} */ (lines[i]))\n i += 1\n }\n i += 1\n out.push('<pre><code>' + highlightCode(buf.join('\\n'), langHint) + '</code></pre>')\n continue\n }\n const h = /^(#{1,6})\\s+(.*)$/.exec(line)\n if (h) {\n const lv = (h[1] || '').length\n out.push('<h' + lv + '>' + mdInline(mdEscape(/** @type {string} */ (h[2]))) + '</h' + lv + '>')\n i += 1\n continue\n }\n if (/^\\s*(---+|\\*\\*\\*+|___+)\\s*$/.test(line)) {\n out.push('<hr>')\n i += 1\n continue\n }\n if (/^\\s*>\\s?/.test(line)) {\n /** @type {string[]} */\n const q = []\n while (i < lines.length && /^\\s*>\\s?/.test(/** @type {string} */ (lines[i]))) {\n q.push((/** @type {string} */ (lines[i])).replace(/^\\s*>\\s?/, ''))\n i += 1\n }\n out.push('<blockquote>' + mdInline(mdEscape(q.join(' '))) + '</blockquote>')\n continue\n }\n if (/^\\s*[-*+]\\s+/.test(line)) {\n /** @type {string[]} */\n const lis = []\n while (i < lines.length && /^\\s*[-*+]\\s+/.test(/** @type {string} */ (lines[i]))) {\n lis.push(mdInline(mdEscape((/** @type {string} */ (lines[i])).replace(/^\\s*[-*+]\\s+/, ''))))\n i += 1\n }\n out.push('<ul>' + lis.map((x) => '<li>' + x + '</li>').join('') + '</ul>')\n continue\n }\n if (/^\\s*\\d+\\.\\s+/.test(line)) {\n /** @type {string[]} */\n const lis2 = []\n while (i < lines.length && /^\\s*\\d+\\.\\s+/.test(/** @type {string} */ (lines[i]))) {\n lis2.push(mdInline(mdEscape((/** @type {string} */ (lines[i])).replace(/^\\s*\\d+\\.\\s+/, ''))))\n i += 1\n }\n out.push('<ol>' + lis2.map((x) => '<li>' + x + '</li>').join('') + '</ol>')\n continue\n }\n if (line.trim() === '') {\n i += 1\n continue\n }\n // GFM 管道表格:表头行(含 |)+ 分隔行(:---/:---:/---:)+ 数据行,\n // 空行或不含 | 的行结束;列数以表头为准,数据行多删少补,\n // 单元格内容走 mdEscape + mdInline,对齐声明转成内联 text-align。\n if (line.indexOf('|') >= 0 && i + 1 < lines.length) {\n const head = mdTrimEdgeCells(mdSplitCells(line))\n const delim = mdTrimEdgeCells(mdSplitCells(/** @type {string} */ (lines[i + 1])))\n if (head.length && mdIsDelimiterRow(delim)) {\n const aligns = delim.map(mdCellAlign)\n const colCount = head.length\n /** @type {string[][]} */\n const rows = []\n i += 2\n while (i < lines.length && (/** @type {string} */ (lines[i])).trim() !== '' && (/** @type {string} */ (lines[i])).indexOf('|') >= 0) {\n const cells = mdTrimEdgeCells(mdSplitCells(/** @type {string} */ (lines[i])))\n /** @type {string[]} */\n const padded = []\n for (let c = 0; c < colCount; c += 1) padded.push(cells[c] || '')\n rows.push(padded)\n i += 1\n }\n let table = '<table><thead><tr>'\n for (let c = 0; c < colCount; c += 1) {\n table += '<th' + mdAlignAttr(aligns[c] || '') + '>' + mdInline(mdEscape(head[c] || '')) + '</th>'\n }\n table += '</tr></thead><tbody>'\n for (const cells of rows) {\n table += '<tr>'\n for (let c = 0; c < colCount; c += 1) {\n table += '<td' + mdAlignAttr(aligns[c] || '') + '>' + mdInline(mdEscape(cells[c] || '')) + '</td>'\n }\n table += '</tr>'\n }\n out.push(table + '</tbody></table>')\n continue\n }\n }\n out.push('<p>' + mdInline(mdEscape(line)) + '</p>')\n i += 1\n }\n return out.join('\\n')\n}\n";
1023
1023
  //#endregion
1024
1024
  //#region src/shared/i18n.js?raw
1025
- var i18n_default = "/**\n * 共享:中英双语 UI 文案。\n *\n * 与 ext/highlight/markdown 一样遵守「可移植源码」约束:只能使用 JSDoc 标注\n * 类型(不得出现 TS 语法注记)、不得引入本目录之外的依赖。本模块被同时用于:\n * 1. tsdown 打包进 client 侧(浏览器 React bundle);\n * 2. tsdown 构建期经 `?raw` 读入原始文本,内联进独立弹出页 /flyout-sidebar\n * 的经典 <script>(见 src/host/page.ts)。\n *\n * 语言选择:跟随 DSH 宿主界面语言。应用内侧边栏观察 `<html lang>`(宿主\n * 已设置语言时),把 'zh'/'en' 通过 localStorage `dsh-flyout-sidebar:lang`\n * 发布;独立弹出页 / 各入口读取该值,未发布时按浏览器语言自动判定。\n * `t(key)` 取当前语言文案,缺失时回退另一语言、再回退 key 本身。\n */\n\n/** @type {Record<string, string>} */\nconst ZH = {\n // 通用状态\n loading: '加载中…',\n loadingTree: '加载文件树…',\n loadingChanges: '加载变更列表…',\n loadingPdf: '加载 PDF…',\n searching: '搜索中…',\n emptyDir: '(空目录)',\n noChanges: '没有未提交的变更',\n noChangesHead: '没有未提交的变更(相对于 HEAD)',\n noResults: '没有匹配的文件',\n readFailed: '读取失败',\n retry: '重试',\n gitStatusFailed: 'git status 失败',\n imageLoadFailed: '图片加载失败',\n pdfLoadFailed: 'pdf.js 加载失败',\n searchFailed: '搜索失败',\n treeLoadFailed: '加载失败',\n truncated: '(已截断的预览)',\n // 复制 / 引用\n copied: '已复制',\n copiedPath: '已复制路径',\n copiedRef: '已复制 @引用(未能写入输入框)',\n insertedInput: '已插入输入框',\n refBtn: '@引用',\n refTitle: '引用到输入框(失败则复制 @path)',\n refFlyoutTitle: '复制 @path 引用',\n copyPath: '复制路径',\n refInput: '@引用到输入框',\n // 面板头部 / 控件\n collapsePanel: '收起侧边栏',\n panelAria: '文件面板',\n flyoutTitle: '弹出式侧边栏',\n flyoutOpen: '弹出式侧边栏 — 在新标签页打开(可拖到另一块显示器)',\n resizeHandle: '拖动调整宽度',\n resizePanel: '拖动调整面板宽度',\n refreshTree: '刷新文件树',\n refreshChanges: '刷新变更列表',\n refresh: '刷新',\n viewGit: '查看 Git 变更(未提交)',\n backToFiles: '返回文件列表',\n hidePreview: '隐藏预览(标签页保留)',\n closeTab: '关闭标签页',\n closeOthers: '关闭其他',\n closeRight: '关闭右侧标签',\n previewRegion: '文件预览',\n diffTabPrefix: '[diff] ',\n searchPlaceholder: '搜索文件…',\n searchToggle: '搜索文件',\n hintClickGit: '点击右侧变更文件查看 diff',\n hintClickTree: '点击右侧文件查看内容',\n movePanelLeft: '将文件面板移到左侧',\n movePanelRight: '将文件面板移到右侧',\n // git 状态\n statusU: '未跟踪',\n statusA: '新增',\n statusM: '修改',\n statusD: '删除',\n statusR: '重命名',\n statusC: '复制',\n statusT: '类型变更',\n staged: '(已暂存)',\n unstaged: '(未暂存)',\n // diff / 预览控件\n diffDeleted: '- 删除',\n diffAdded: '+ 新增',\n zoomOut: '缩小',\n zoomIn: '放大',\n prevPage: '上一页',\n nextPage: '下一页',\n openInEditor: '在系统编辑器打开',\n openedInEditor: '已在编辑器打开',\n openFailed: '打开失败',\n // 设置卡片\n settingsTitle: '弹出式侧边栏',\n settingsDesc: '侧边栏面板偏好设置',\n settingsAutoRefresh: '打开面板时自动刷新',\n settingsMinWidth: '面板最小宽度(%)',\n settingsDefaultOpen: '页面加载后默认展开',\n settingsFontSize: '内容区字号(px)',\n}\n\n/** @type {Record<string, string>} */\nconst EN = {\n loading: 'Loading…',\n loadingTree: 'Loading file tree…',\n loadingChanges: 'Loading change list…',\n loadingPdf: 'Loading PDF…',\n searching: 'Searching…',\n emptyDir: '(empty directory)',\n noChanges: 'No uncommitted changes',\n noChangesHead: 'No uncommitted changes (relative to HEAD)',\n noResults: 'No matching files',\n readFailed: 'Failed to read',\n retry: 'Retry',\n gitStatusFailed: 'git status failed',\n imageLoadFailed: 'Failed to load image',\n pdfLoadFailed: 'Failed to load pdf.js',\n searchFailed: 'Search failed',\n treeLoadFailed: 'Failed to load',\n truncated: '(truncated preview)',\n copied: 'Copied',\n copiedPath: 'Path copied',\n copiedRef: 'Copied @reference (could not write to input box)',\n insertedInput: 'Inserted into input box',\n refBtn: '@ref',\n refTitle: 'Quote into input box (falls back to copying @path)',\n refFlyoutTitle: 'Copy @path reference',\n copyPath: 'Copy path',\n refInput: '@reference into input box',\n collapsePanel: 'Collapse sidebar',\n panelAria: 'File panel',\n flyoutTitle: 'Flyout sidebar',\n flyoutOpen: 'Flyout sidebar — open in a new tab (drag to another display)',\n resizeHandle: 'Drag to resize',\n resizePanel: 'Drag to resize panel',\n refreshTree: 'Refresh file tree',\n refreshChanges: 'Refresh change list',\n refresh: 'Refresh',\n viewGit: 'View Git changes (uncommitted)',\n backToFiles: 'Back to file list',\n hidePreview: 'Hide preview (tabs are kept)',\n closeTab: 'Close tab',\n closeOthers: 'Close others',\n closeRight: 'Close tabs to the right',\n previewRegion: 'File preview',\n diffTabPrefix: '[diff] ',\n searchPlaceholder: 'Search files…',\n searchToggle: 'Search files',\n hintClickGit: 'Click a changed file to view its diff',\n hintClickTree: 'Click a file to view its content',\n movePanelLeft: 'Move file panel to the left',\n movePanelRight: 'Move file panel to the right',\n statusU: 'Untracked',\n statusA: 'Added',\n statusM: 'Modified',\n statusD: 'Deleted',\n statusR: 'Renamed',\n statusC: 'Copied',\n statusT: 'Type change',\n staged: '(staged)',\n unstaged: '(unstaged)',\n diffDeleted: '- Deleted',\n diffAdded: '+ Added',\n zoomOut: 'Zoom out',\n zoomIn: 'Zoom in',\n prevPage: 'Previous page',\n nextPage: 'Next page',\n openInEditor: 'Open in system editor',\n openedInEditor: 'Opened in editor',\n openFailed: 'Failed to open',\n // Settings card\n settingsTitle: 'Flyout Sidebar',\n settingsDesc: 'Sidebar panel preferences',\n settingsAutoRefresh: 'Auto-refresh on panel open',\n settingsMinWidth: 'Min panel width (%)',\n settingsDefaultOpen: 'Open by default on page load',\n settingsFontSize: 'Content font size (px)',\n}\n\n/** @type {Record<string, Record<string, string>>} */\nconst DICTS = { zh: ZH, en: EN }\n\nconst LANG_KEY = 'dsh-flyout-sidebar:lang'\n\n/** @type {'zh' | 'en' | null} 已发布的主界面语言(null = 宿主未发布,按浏览器判定) */\nlet publishedLang = null\n/** @type {Array<(lang: string) => void>} */\nlet listeners = []\n\n/** @returns {'zh' | 'en' | null} 主面板发布到 localStorage 的语言,无效时 null */\nfunction readPublishedLang() {\n try {\n var v = localStorage.getItem(LANG_KEY)\n return v === 'zh' || v === 'en' ? v : null\n } catch (e) {\n return null\n }\n}\n\n/** 浏览器语言自动判定:非 zh 开头一律英文 */\nfunction autoLang() {\n try {\n var langs = navigator.languages || (navigator.language ? [navigator.language] : [])\n for (var i = 0; i < langs.length; i++) {\n var l = String(langs[i] || '').toLowerCase()\n if (l.indexOf('zh') === 0) return 'zh'\n if (l.indexOf('en') === 0) return 'en'\n }\n } catch (e) {\n // navigator 不可用时按英文处理\n }\n return 'en'\n}\n\nfunction currentLang() {\n if (publishedLang === null) publishedLang = readPublishedLang()\n return publishedLang || autoLang()\n}\n\n/**\n * @param {string} key 文案键\n * @returns {string} 当前语言文案;两级回退(另一语言 → key)\n */\nexport function t(key) {\n var lang = currentLang()\n var dict = DICTS[lang] || EN\n if (dict[key] != null) return dict[key]\n var other = DICTS[lang === 'zh' ? 'en' : 'zh']\n if (!other) return key\n return other[key] != null ? other[key] : key\n}\n\n/** @returns {'zh' | 'en'} 当前生效语言(含自动判定的结果) */\nexport function getLang() {\n return currentLang()\n}\n\n/**\n * 发布语言:主面板观察宿主界面语言后调用,写入 localStorage 供独立弹出页\n * 读取,并通知本页订阅者重渲染。\n * @param {'zh' | 'en'} lang\n */\nexport function setLang(lang) {\n publishedLang = lang\n try {\n localStorage.setItem(LANG_KEY, lang)\n } catch (e) {\n // localStorage 不可用时仅保存在内存\n }\n var next = listeners.slice()\n for (var i = 0; i < next.length; i++) {\n var fn = next[i]\n if (!fn) continue\n try {\n fn(lang)\n } catch (e) {\n // 单个订阅者异常不阻断其余\n }\n }\n}\n\n/** @param {(lang: string) => void} fn @returns {() => void} 取消订阅 */\nexport function subscribeLang(fn) {\n listeners.push(fn)\n return function () {\n listeners = listeners.filter(function (f) {\n return f !== fn\n })\n }\n}\n";
1025
+ var i18n_default = "/**\n * 共享:中英双语 UI 文案。\n *\n * 与 ext/highlight/markdown 一样遵守「可移植源码」约束:只能使用 JSDoc 标注\n * 类型(不得出现 TS 语法注记)、不得引入本目录之外的依赖。本模块被同时用于:\n * 1. tsdown 打包进 client 侧(浏览器 React bundle);\n * 2. tsdown 构建期经 `?raw` 读入原始文本,内联进独立弹出页 /flyout-sidebar\n * 的经典 <script>(见 src/host/page.ts)。\n *\n * 语言选择:跟随 DSH 宿主界面语言。应用内侧边栏观察 `<html lang>`(宿主\n * 已设置语言时),把 'zh'/'en' 通过 localStorage `dsh-flyout-sidebar:lang`\n * 发布;独立弹出页 / 各入口读取该值,未发布时按浏览器语言自动判定。\n * `t(key)` 取当前语言文案,缺失时回退另一语言、再回退 key 本身。\n */\n\n/** @type {Record<string, string>} */\nconst ZH = {\n // 通用状态\n loading: '加载中…',\n loadingTree: '加载文件树…',\n loadingChanges: '加载变更列表…',\n loadingPdf: '加载 PDF…',\n searching: '搜索中…',\n emptyDir: '(空目录)',\n noChanges: '没有未提交的变更',\n noChangesHead: '没有未提交的变更(相对于 HEAD)',\n noResults: '没有匹配的文件',\n readFailed: '读取失败',\n retry: '重试',\n gitStatusFailed: 'git status 失败',\n imageLoadFailed: '图片加载失败',\n pdfLoadFailed: 'pdf.js 加载失败',\n searchFailed: '搜索失败',\n treeLoadFailed: '加载失败',\n truncated: '(已截断的预览)',\n // 复制 / 引用\n copied: '已复制',\n copiedPath: '已复制路径',\n copiedRef: '已复制 @引用(未能写入输入框)',\n insertedInput: '已插入输入框',\n refBtn: '@引用',\n refTitle: '引用到输入框(失败则复制 @path)',\n refFlyoutTitle: '复制 @path 引用',\n copyPath: '复制路径',\n refInput: '@引用到输入框',\n // 面板头部 / 控件\n collapsePanel: '收起侧边栏',\n panelAria: '文件面板',\n flyoutTitle: '弹出式侧边栏',\n flyoutOpen: '弹出式侧边栏 — 在新标签页打开(可拖到另一块显示器)',\n resizeHandle: '拖动调整宽度',\n resizePanel: '拖动调整面板宽度',\n refreshTree: '刷新文件树',\n refreshChanges: '刷新变更列表',\n refresh: '刷新',\n viewGit: '查看 Git 变更(未提交)',\n backToFiles: '返回文件列表',\n hidePreview: '隐藏预览(标签页保留)',\n closeTab: '关闭标签页',\n closeOthers: '关闭其他',\n closeRight: '关闭右侧标签',\n previewRegion: '文件预览',\n diffTabPrefix: '[diff] ',\n searchPlaceholder: '搜索文件…',\n searchToggle: '搜索文件',\n hintClickGit: '点击右侧变更文件查看 diff',\n hintClickTree: '点击右侧文件查看内容',\n movePanelLeft: '将文件面板移到左侧',\n movePanelRight: '将文件面板移到右侧',\n // git 状态\n statusU: '未跟踪',\n statusA: '新增',\n statusM: '修改',\n statusD: '删除',\n statusR: '重命名',\n statusC: '复制',\n statusT: '类型变更',\n staged: '(已暂存)',\n unstaged: '(未暂存)',\n // diff / 预览控件\n diffDeleted: '- 删除',\n diffAdded: '+ 新增',\n zoomOut: '缩小',\n zoomIn: '放大',\n prevPage: '上一页',\n nextPage: '下一页',\n openInEditor: '在系统编辑器打开',\n openedInEditor: '已在编辑器打开',\n openFailed: '打开失败',\n // 插件配置页(插件页 → 本插件的行 → 配置)\n settingsTitle: '弹出式侧边栏',\n settingsDesc: '侧边栏面板偏好设置',\n settingsAutoRefresh: '打开面板时自动刷新',\n settingsMinWidth: '面板最小宽度(%)',\n settingsDefaultOpen: '页面加载后默认展开',\n settingsFontSize: '内容区字号(px)',\n settingsSave: '保存',\n settingsSaving: '保存中…',\n settingsSaveFailed: '宿主未接受这些值,已保留供你修改。',\n settingsReadonly: '本部署的设置为只读。',\n settingsUnavailable: '该插件当前未加载,暂时无法配置。',\n settingsInvalidNumber: '请填范围内的数字;留空表示恢复默认。',\n}\n\n/** @type {Record<string, string>} */\nconst EN = {\n loading: 'Loading…',\n loadingTree: 'Loading file tree…',\n loadingChanges: 'Loading change list…',\n loadingPdf: 'Loading PDF…',\n searching: 'Searching…',\n emptyDir: '(empty directory)',\n noChanges: 'No uncommitted changes',\n noChangesHead: 'No uncommitted changes (relative to HEAD)',\n noResults: 'No matching files',\n readFailed: 'Failed to read',\n retry: 'Retry',\n gitStatusFailed: 'git status failed',\n imageLoadFailed: 'Failed to load image',\n pdfLoadFailed: 'Failed to load pdf.js',\n searchFailed: 'Search failed',\n treeLoadFailed: 'Failed to load',\n truncated: '(truncated preview)',\n copied: 'Copied',\n copiedPath: 'Path copied',\n copiedRef: 'Copied @reference (could not write to input box)',\n insertedInput: 'Inserted into input box',\n refBtn: '@ref',\n refTitle: 'Quote into input box (falls back to copying @path)',\n refFlyoutTitle: 'Copy @path reference',\n copyPath: 'Copy path',\n refInput: '@reference into input box',\n collapsePanel: 'Collapse sidebar',\n panelAria: 'File panel',\n flyoutTitle: 'Flyout sidebar',\n flyoutOpen: 'Flyout sidebar — open in a new tab (drag to another display)',\n resizeHandle: 'Drag to resize',\n resizePanel: 'Drag to resize panel',\n refreshTree: 'Refresh file tree',\n refreshChanges: 'Refresh change list',\n refresh: 'Refresh',\n viewGit: 'View Git changes (uncommitted)',\n backToFiles: 'Back to file list',\n hidePreview: 'Hide preview (tabs are kept)',\n closeTab: 'Close tab',\n closeOthers: 'Close others',\n closeRight: 'Close tabs to the right',\n previewRegion: 'File preview',\n diffTabPrefix: '[diff] ',\n searchPlaceholder: 'Search files…',\n searchToggle: 'Search files',\n hintClickGit: 'Click a changed file to view its diff',\n hintClickTree: 'Click a file to view its content',\n movePanelLeft: 'Move file panel to the left',\n movePanelRight: 'Move file panel to the right',\n statusU: 'Untracked',\n statusA: 'Added',\n statusM: 'Modified',\n statusD: 'Deleted',\n statusR: 'Renamed',\n statusC: 'Copied',\n statusT: 'Type change',\n staged: '(staged)',\n unstaged: '(unstaged)',\n diffDeleted: '- Deleted',\n diffAdded: '+ Added',\n zoomOut: 'Zoom out',\n zoomIn: 'Zoom in',\n prevPage: 'Previous page',\n nextPage: 'Next page',\n openInEditor: 'Open in system editor',\n openedInEditor: 'Opened in editor',\n openFailed: 'Failed to open',\n // Plugins page → this bundle's row → configure\n settingsTitle: 'Flyout Sidebar',\n settingsDesc: 'Sidebar panel preferences',\n settingsAutoRefresh: 'Auto-refresh on panel open',\n settingsMinWidth: 'Min panel width (%)',\n settingsDefaultOpen: 'Open by default on page load',\n settingsFontSize: 'Content font size (px)',\n settingsSave: 'Save',\n settingsSaving: 'Saving…',\n settingsSaveFailed: 'The deployment did not accept these values; they were left for you to correct.',\n settingsReadonly: 'This deployment stores settings read-only.',\n settingsUnavailable: 'This plugin is not loaded, so it cannot be configured right now.',\n settingsInvalidNumber: 'Enter a number within range, or leave blank to use the default.',\n}\n\n/** @type {Record<string, Record<string, string>>} */\nconst DICTS = { zh: ZH, en: EN }\n\nconst LANG_KEY = 'dsh-flyout-sidebar:lang'\n\n/** @type {'zh' | 'en' | null} 已发布的主界面语言(null = 宿主未发布,按浏览器判定) */\nlet publishedLang = null\n/** @type {Array<(lang: string) => void>} */\nlet listeners = []\n\n/** @returns {'zh' | 'en' | null} 主面板发布到 localStorage 的语言,无效时 null */\nfunction readPublishedLang() {\n try {\n var v = localStorage.getItem(LANG_KEY)\n return v === 'zh' || v === 'en' ? v : null\n } catch (e) {\n return null\n }\n}\n\n/** 浏览器语言自动判定:非 zh 开头一律英文 */\nfunction autoLang() {\n try {\n var langs = navigator.languages || (navigator.language ? [navigator.language] : [])\n for (var i = 0; i < langs.length; i++) {\n var l = String(langs[i] || '').toLowerCase()\n if (l.indexOf('zh') === 0) return 'zh'\n if (l.indexOf('en') === 0) return 'en'\n }\n } catch (e) {\n // navigator 不可用时按英文处理\n }\n return 'en'\n}\n\nfunction currentLang() {\n if (publishedLang === null) publishedLang = readPublishedLang()\n return publishedLang || autoLang()\n}\n\n/**\n * @param {string} key 文案键\n * @returns {string} 当前语言文案;两级回退(另一语言 → key)\n */\nexport function t(key) {\n var lang = currentLang()\n var dict = DICTS[lang] || EN\n if (dict[key] != null) return dict[key]\n var other = DICTS[lang === 'zh' ? 'en' : 'zh']\n if (!other) return key\n return other[key] != null ? other[key] : key\n}\n\n/** @returns {'zh' | 'en'} 当前生效语言(含自动判定的结果) */\nexport function getLang() {\n return currentLang()\n}\n\n/**\n * 发布语言:主面板观察宿主界面语言后调用,写入 localStorage 供独立弹出页\n * 读取,并通知本页订阅者重渲染。\n * @param {'zh' | 'en'} lang\n */\nexport function setLang(lang) {\n publishedLang = lang\n try {\n localStorage.setItem(LANG_KEY, lang)\n } catch (e) {\n // localStorage 不可用时仅保存在内存\n }\n var next = listeners.slice()\n for (var i = 0; i < next.length; i++) {\n var fn = next[i]\n if (!fn) continue\n try {\n fn(lang)\n } catch (e) {\n // 单个订阅者异常不阻断其余\n }\n }\n}\n\n/** @param {(lang: string) => void} fn @returns {() => void} 取消订阅 */\nexport function subscribeLang(fn) {\n listeners.push(fn)\n return function () {\n listeners = listeners.filter(function (f) {\n return f !== fn\n })\n }\n}\n";
1026
1026
  //#endregion
1027
1027
  //#region src/shared/gitui.js?raw
1028
1028
  var gitui_default = "/**\n * 共享:git 变更列表 / diff 的纯展示逻辑。\n *\n * client 侧 React 组件与独立弹出页(page.ts 内联脚本)共用,避免双端各自\n * 实现后细节漂移。与 ext/highlight/markdown 一样遵守「可移植源码」约束:\n * 只能使用 JSDoc 标注类型、依赖仅限本目录(i18n 的 t 在内联场景由同作用域\n * 函数声明提升提供)。\n */\nimport { t } from './i18n.js'\n\n/** @param {string} p @returns {string} */\nexport function basename(p) {\n var s = String(p).replace(/[\\\\/]+$/, '')\n var i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\\\'))\n return i >= 0 ? s.slice(i + 1) : s\n}\n\n/**\n * 变更文件的状态徽章字母:未跟踪为 U,否则取工作树状态(y),再退到暂存\n * 状态(x),空缺按 M。\n * @param {{ x: string, y: string }} e @returns {string}\n */\nexport function gitLabel(e) {\n if (e.x === '?' || e.y === '?') return 'U'\n return (e.y !== ' ' ? e.y : e.x) || 'M'\n}\n\n/**\n * 变更文件行 hover 提示:状态名 + 暂存/未暂存。\n * @param {{ x: string, y: string }} e @returns {string}\n */\nexport function gitTitle(e) {\n var label = gitLabel(e)\n /** @type {Record<string, string>} */\n var map = {\n U: t('statusU'), A: t('statusA'), M: t('statusM'),\n D: t('statusD'), R: t('statusR'), C: t('statusC'), T: t('statusT'),\n }\n var staged = e.x !== ' ' && e.x !== '?'\n return (map[label] || label) + (staged ? t('staged') : t('unstaged'))\n}\n\n/**\n * unified diff 单行的样式类:hunk / 增 / 删 / 元信息(diff --git、index、\n * rename 等),其余为普通行。\n * @param {string} line @returns {string}\n */\nexport function gitDiffLineClass(line) {\n var cls = 'gd-line'\n if (line.indexOf('@@') === 0) cls += ' gd-hunk'\n else if (line.charAt(0) === '+' && line.indexOf('+++') !== 0) cls += ' gd-add'\n else if (line.charAt(0) === '-' && line.indexOf('---') !== 0) cls += ' gd-del'\n else if (\n line.indexOf('diff ') === 0 || line.indexOf('index ') === 0 || line.indexOf('--- ') === 0 ||\n line.indexOf('+++ ') === 0 || line.indexOf('new file') === 0 || line.indexOf('deleted file') === 0 ||\n line.indexOf('old mode') === 0 || line.indexOf('new mode') === 0 || line.indexOf('rename ') === 0 ||\n line.indexOf('similarity ') === 0 || line.indexOf('copy ') === 0 || line.indexOf('Binary files') === 0 ||\n line.charAt(0) === '\\\\'\n ) {\n cls += ' gd-meta'\n }\n return cls\n}\n";
@@ -1098,7 +1098,7 @@ function sendText(res, status, body) {
1098
1098
  res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
1099
1099
  res.end(body);
1100
1100
  }
1101
- function registerRoutes(ctx, webServer, _config, getConfig) {
1101
+ function registerRoutes(ctx, webServer, getConfig) {
1102
1102
  const register = (route, label) => {
1103
1103
  ctx.effect(() => webServer.register(route), label);
1104
1104
  };
@@ -1252,7 +1252,7 @@ function registerRoutes(ctx, webServer, _config, getConfig) {
1252
1252
  }, "artifacts: pdf.js worker route");
1253
1253
  }
1254
1254
  //#endregion
1255
- //#region node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.3/node_modules/@deepseek-ai/cosmokit/lib/index.js
1255
+ //#region node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.5/node_modules/@deepseek-ai/cosmokit/lib/index.js
1256
1256
  /** Return true when a value is `null` or `undefined`. */
1257
1257
  function isNullable(value) {
1258
1258
  return value === null || value === void 0;
@@ -1276,6 +1276,43 @@ function pick(source, keys, forced) {
1276
1276
  for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
1277
1277
  return result;
1278
1278
  }
1279
+ /** Shared config references used by schema validators and plugin runtimes. */
1280
+ const write = Symbol.for("cosmokit.volatile.write");
1281
+ function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
1282
+ if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
1283
+ if (value === null || typeof value !== "object") return value;
1284
+ if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
1285
+ ancestors.add(value);
1286
+ try {
1287
+ if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
1288
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
1289
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
1290
+ } finally {
1291
+ ancestors.delete(value);
1292
+ }
1293
+ }
1294
+ /**
1295
+ * Create a detached reference containing an immutable copy of the supplied data.
1296
+ * @param value - validated config data; class instances and functions are unsupported.
1297
+ * @returns a reference whose value is updated only by its owning runtime.
1298
+ */
1299
+ function createVolatile(value) {
1300
+ let current = snapshot(value);
1301
+ return Object.freeze({
1302
+ get: () => current,
1303
+ [write]: (value) => {
1304
+ current = value;
1305
+ }
1306
+ });
1307
+ }
1308
+ /**
1309
+ * Identify references across ESM/CJS copies of the shared library.
1310
+ * @param value - a parsed config value.
1311
+ * @returns whether the value implements the shared reference protocol.
1312
+ */
1313
+ function isVolatile(value) {
1314
+ return typeof value === "object" && value !== null && write in value;
1315
+ }
1279
1316
  /** Test values using `instanceof` with a `toStringTag` fallback. */
1280
1317
  function is(type, value) {
1281
1318
  if (arguments.length === 1) return (value) => is(type, value);
@@ -1356,26 +1393,47 @@ function clone(source, refs = /* @__PURE__ */ new Map()) {
1356
1393
  }
1357
1394
  return result;
1358
1395
  }
1359
- /** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
1396
+ /**
1397
+ * Compare values recursively, treating two volatile references as equal regardless of value.
1398
+ * Strict comparison distinguishes null/undefined, treats opaque objects by identity,
1399
+ * compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
1400
+ * @param a - first value.
1401
+ * @param b - second value.
1402
+ * @param strict - whether to require strict data equality outside volatile references.
1403
+ * @returns whether the values compare equal.
1404
+ */
1360
1405
  function deepEqual(a, b, strict) {
1361
- if (a === b) return true;
1362
- if (!strict && isNullable(a) && isNullable(b)) return true;
1363
- if (typeof a !== typeof b) return false;
1364
- if (typeof a !== "object") return false;
1365
- if (!a || !b) return false;
1366
- function check(test, then) {
1367
- return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
1406
+ const ancestors = /* @__PURE__ */ new Set();
1407
+ function compare(a, b) {
1408
+ if (a === b) return true;
1409
+ if (isVolatile(a) || isVolatile(b)) return isVolatile(a) && isVolatile(b);
1410
+ if (!strict && isNullable(a) && isNullable(b)) return true;
1411
+ if (typeof a !== typeof b || typeof a !== "object" || !a || !b) return false;
1412
+ if (ancestors.has(a)) return false;
1413
+ function check(test, then) {
1414
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
1415
+ }
1416
+ ancestors.add(a);
1417
+ try {
1418
+ return check(Array.isArray, (a, b) => {
1419
+ if (a.length !== b.length) return false;
1420
+ for (let index = 0; index < a.length; index++) if (!compare(a[index], b[index])) return false;
1421
+ return true;
1422
+ }) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("URL"), (a, b) => a.href === b.href) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
1423
+ if (a.byteLength !== b.byteLength) return false;
1424
+ const viewA = new Uint8Array(a);
1425
+ const viewB = new Uint8Array(b);
1426
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
1427
+ return true;
1428
+ }) ?? ((!strict || [a, b].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
1429
+ ...a,
1430
+ ...b
1431
+ }).every((key) => compare(a[key], b[key])));
1432
+ } finally {
1433
+ ancestors.delete(a);
1434
+ }
1368
1435
  }
1369
- return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
1370
- if (a.byteLength !== b.byteLength) return false;
1371
- const viewA = new Uint8Array(a);
1372
- const viewB = new Uint8Array(b);
1373
- for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
1374
- return true;
1375
- }) ?? Object.keys({
1376
- ...a,
1377
- ...b
1378
- }).every((key) => deepEqual(a[key], b[key], strict));
1436
+ return compare(a, b);
1379
1437
  }
1380
1438
  /** Time constants plus parsing and formatting helpers. */
1381
1439
  var Time;
@@ -1448,7 +1506,7 @@ var Time;
1448
1506
  Time.template = template;
1449
1507
  })(Time || (Time = {}));
1450
1508
  //#endregion
1451
- //#region node_modules/.pnpm/@deepseek-ai+schemastery@3.18.2/node_modules/@deepseek-ai/schemastery/lib/index.mjs
1509
+ //#region node_modules/.pnpm/@deepseek-ai+schemastery@3.18.4/node_modules/@deepseek-ai/schemastery/lib/index.mjs
1452
1510
  const kSchema = Symbol.for("schemastery");
1453
1511
  const kValidationError = Symbol.for("ValidationError");
1454
1512
  globalThis.__schemastery_index__ ??= 0;
@@ -1624,6 +1682,7 @@ Schema.prototype.pattern = function pattern(regexp) {
1624
1682
  return schema;
1625
1683
  };
1626
1684
  Schema.prototype.simplify = function simplify(value) {
1685
+ if (isVolatile(value)) value = value.get();
1627
1686
  if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
1628
1687
  if (isNullable(value)) return value;
1629
1688
  if (this.type === "object" || this.type === "dict") {
@@ -1680,12 +1739,49 @@ for (const key of [
1680
1739
  };
1681
1740
  return schema;
1682
1741
  } });
1742
+ Schema.prototype.volatile = function volatile() {
1743
+ if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
1744
+ return this.extra("volatile", true);
1745
+ };
1683
1746
  const resolvers = {};
1747
+ const checkedVolatile = Symbol("checked-volatile-schema");
1748
+ function validateVolatileSchema(schema, path = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
1749
+ const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
1750
+ if (states.has(blocked)) return;
1751
+ states.add(blocked);
1752
+ seen.set(schema, states);
1753
+ if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path });
1754
+ const nested = blocked || !!schema.meta?.volatile;
1755
+ if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path, key], nested, seen);
1756
+ if (schema.sKey) validateVolatileSchema(schema.sKey, [...path, "<key>"], true, seen);
1757
+ if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path, "*"], true, seen);
1758
+ if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path, String(index)], true, seen);
1759
+ }
1684
1760
  Schema.extend = function extend(type, resolve) {
1685
1761
  resolvers[type] = resolve;
1686
1762
  };
1687
1763
  Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
1688
1764
  if (!schema) return [data];
1765
+ if (!options[checkedVolatile]) {
1766
+ validateVolatileSchema(schema, options.path);
1767
+ options = {
1768
+ ...options,
1769
+ [checkedVolatile]: true
1770
+ };
1771
+ }
1772
+ if (schema.meta?.volatile) {
1773
+ const inner = Schema(schema);
1774
+ inner.meta = {
1775
+ ...schema.meta,
1776
+ volatile: false
1777
+ };
1778
+ const [value, adapted] = Schema.resolve(data, inner, options, strict);
1779
+ try {
1780
+ return [createVolatile(value), adapted];
1781
+ } catch (error) {
1782
+ throw new ValidationError(error instanceof Error ? error.message : String(error), options);
1783
+ }
1784
+ }
1689
1785
  if (options.ignore?.(data, schema)) return [data];
1690
1786
  if (isNullable(data) && schema.type !== "lazy") {
1691
1787
  if (schema.meta.required) throw new ValidationError(`missing required value`, options);
@@ -1788,6 +1884,7 @@ Schema.extend("lazy", (data, schema, options, strict) => {
1788
1884
  ...schema.meta,
1789
1885
  ...schema.inner.meta
1790
1886
  };
1887
+ validateVolatileSchema(schema.inner, options.path, true);
1791
1888
  }
1792
1889
  return Schema.resolve(data, schema.inner, options, strict);
1793
1890
  });
@@ -1887,7 +1984,7 @@ function property(data, key, schema, options) {
1887
1984
  } catch (e) {
1888
1985
  if (!options?.autofix) throw e;
1889
1986
  delete data[key];
1890
- return schema.meta.default;
1987
+ return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
1891
1988
  }
1892
1989
  }
1893
1990
  Schema.extend("array", (data, { inner, meta }, options) => {
@@ -2051,14 +2148,31 @@ defineMethod("transform", [
2051
2148
  * 兼容(静态 bundle 下 harness 全局不存在,typeof 守卫直接跳过)。
2052
2149
  */
2053
2150
  const name = "dsh-flyout-sidebar";
2151
+ /**
2152
+ * 插件配置 schema。字段以 `.volatile()` 声明:DSH Settings → Plugins 的表单只投影
2153
+ * volatile 字段(按 profile 条目 id `flyout-sidebar` 定位本插件),编辑结果写进当前
2154
+ * profile 的 Cordis patch。apply 收到的配置里 volatile 字段是稳定引用,读当前值必须
2155
+ * 走 `.get()`;引用由 loader 在配置变更时就地更新,插件无需重新加载。
2156
+ */
2054
2157
  const Config = Schema.object({
2055
- autoRefresh: Schema.boolean().default(true).description("打开面板时自动刷新产物与 git 变更"),
2056
- minPanelWidth: Schema.number().min(15).max(60).default(20).description("最短面板宽度(占窗口宽度百分比)"),
2057
- defaultOpen: Schema.boolean().default(true).description("页面加载后默认展开面板"),
2058
- contentFontSize: Schema.number().min(11).max(20).default(13).description("内容区字体大小(px),界面文字不受影响")
2158
+ /** 面板打开时轮询刷新产物与 git 变更 */
2159
+ autoRefresh: Schema.boolean().default(true).description("打开面板时自动刷新产物与 git 变更").volatile(),
2160
+ /** 面板最小宽度(占窗口宽度的百分比,15–60) */
2161
+ minPanelWidth: Schema.number().min(15).max(60).default(20).description("最短面板宽度(占窗口宽度百分比)").volatile(),
2162
+ /** 页面加载后默认展开面板 */
2163
+ defaultOpen: Schema.boolean().default(true).description("页面加载后默认展开面板").volatile(),
2164
+ /** 内容区(代码/diff/markdown)基准字号(px) */
2165
+ contentFontSize: Schema.number().min(11).max(20).default(13).description("内容区字体大小(px),界面文字不受影响").volatile()
2059
2166
  });
2060
- /** settings namespace(DSH Settings 卡片的 join key,与客户端 settingsScope.bind 一致) */
2061
- const SETTINGS_NAMESPACE = "dsh-flyout-sidebar";
2167
+ /** 读取 volatile 引用的当前快照;每次请求现读,用户在 Settings 改值后立即生效 */
2168
+ function readConfig(cfg) {
2169
+ return {
2170
+ autoRefresh: cfg.autoRefresh.get(),
2171
+ minPanelWidth: cfg.minPanelWidth.get(),
2172
+ defaultOpen: cfg.defaultOpen.get(),
2173
+ contentFontSize: cfg.contentFontSize.get()
2174
+ };
2175
+ }
2062
2176
  const inject = [
2063
2177
  "webServer",
2064
2178
  "sessionQuery",
@@ -2068,11 +2182,6 @@ function apply(ctx, config) {
2068
2182
  const cfg = config ?? Config({});
2069
2183
  attachArtifactTracking(ctx);
2070
2184
  attachGitTracking(ctx);
2071
- let settingsScope;
2072
- ctx.inject(["settings"], (sctx) => {
2073
- const settings = sctx.get("settings");
2074
- if (settings) settingsScope = settings.register(SETTINGS_NAMESPACE, Config);
2075
- });
2076
2185
  if (typeof harness !== "undefined" && harness) {
2077
2186
  harness.handle("artifacts.list", () => ({ artifacts: snapshotArtifacts() }));
2078
2187
  harness.handle("artifacts.remove", (args) => removeFile(args?.path));
@@ -2084,9 +2193,7 @@ function apply(ctx, config) {
2084
2193
  harness.handle("git.diff", (args) => gitDiff(ctx, args?.path, args?.sessionId));
2085
2194
  }
2086
2195
  const webServer = ctx.get("webServer");
2087
- if (webServer) registerRoutes(ctx, webServer, cfg, () => {
2088
- return settingsScope?.get() ?? cfg;
2089
- });
2196
+ if (webServer) registerRoutes(ctx, webServer, () => readConfig(cfg));
2090
2197
  }
2091
2198
  //#endregion
2092
- export { Config, SETTINGS_NAMESPACE, apply, inject, name };
2199
+ export { Config, apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-flyout-sidebar",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "可弹出侧边栏 · A DeepSeek Harness sidebar that shows artifacts and pops out into a larger web tab.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,7 +39,7 @@
39
39
  "node": ">=20"
40
40
  },
41
41
  "devDependencies": {
42
- "@deepseek-ai/schemastery": "^3.18.2",
42
+ "@deepseek-ai/schemastery": "^3.18.4",
43
43
  "@types/node": "^26.4.0",
44
44
  "@types/react": "^19.2.18",
45
45
  "happy-dom": "^20.11.10",