dsh-flyout-sidebar 0.1.9 → 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 +238 -129
  2. package/dist/index.js +145 -38
  3. package/package.json +2 -2
package/dist/client.js CHANGED
@@ -358,17 +358,9 @@ body[data-ds-dark-theme] .tok-function, body[data-ds-dark-theme] .tok-decorator
358
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; }
359
359
  body[data-ds-dark-theme] .tok-property { color: #ced4da; }
360
360
 
361
- /* Settings 卡片(Settings → Plugins → Plugin configuration)— 跟随 DSH PluginCard 风格 */
362
- .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); }
363
- .fs-settings-card:hover { border-color: var(--dsw-alias-label-dimmed); }
364
- .fs-settings-open, .fs-settings-open:hover { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-label-dimmed); }
365
- .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; }
366
- .fs-settings-headtext { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
367
- .fs-settings-name { font-size: 15px; font-weight: 600; line-height: 1.4; color: var(--dsw-alias-label-primary); }
368
- .fs-settings-desc { font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
369
- .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); }
370
- .fs-settings-open .fs-settings-chevron { transform: rotate(180deg); }
371
- .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; }
372
364
  .fs-settings-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; }
373
365
  .fs-settings-label { flex: 1; min-width: 0; font-size: 14px; color: var(--dsw-alias-label-primary); }
374
366
  .fs-settings-input {
@@ -380,6 +372,7 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
380
372
  }
381
373
  .fs-settings-input:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
382
374
  .fs-settings-input:disabled { opacity: .5; cursor: default; }
375
+ .fs-settings-input[data-invalid="true"] { border-color: var(--dsw-alias-accent-error, #e5484d); }
383
376
  .fs-settings-toggle {
384
377
  position: relative; width: 40px; height: 22px; border-radius: 11px;
385
378
  border: none; cursor: pointer; padding: 0; flex: none;
@@ -393,6 +386,18 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
393
386
  background: #fff; transition: transform var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out);
394
387
  }
395
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; }
396
401
  `;
397
402
  /** 注入样式(幂等:内容不一致时替换,使热重载后新样式必然生效) */
398
403
  function insertStyles() {
@@ -554,7 +559,13 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
554
559
  settingsAutoRefresh: "打开面板时自动刷新",
555
560
  settingsMinWidth: "面板最小宽度(%)",
556
561
  settingsDefaultOpen: "页面加载后默认展开",
557
- settingsFontSize: "内容区字号(px)"
562
+ settingsFontSize: "内容区字号(px)",
563
+ settingsSave: "保存",
564
+ settingsSaving: "保存中…",
565
+ settingsSaveFailed: "宿主未接受这些值,已保留供你修改。",
566
+ settingsReadonly: "本部署的设置为只读。",
567
+ settingsUnavailable: "该插件当前未加载,暂时无法配置。",
568
+ settingsInvalidNumber: "请填范围内的数字;留空表示恢复默认。"
558
569
  };
559
570
  /** @type {Record<string, string>} */
560
571
  const EN = {
@@ -630,7 +641,13 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
630
641
  settingsAutoRefresh: "Auto-refresh on panel open",
631
642
  settingsMinWidth: "Min panel width (%)",
632
643
  settingsDefaultOpen: "Open by default on page load",
633
- 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."
634
651
  };
635
652
  /** @type {Record<string, Record<string, string>>} */
636
653
  const DICTS = {
@@ -878,11 +895,11 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
878
895
  * 从宿主拉取插件配置(config 已由 cordis 校验填充默认值)并通知订阅者。
879
896
  * 配置保存在宿主侧,页面内不持久化;面板打开即用最新值。
880
897
  *
881
- * syncDefaultOpen=true(启动首次加载、settings scope 配置变更回调)时,
882
- * 若本次拉取确实改动了 defaultOpen 字段,则实时同步边栏开合 —— 用户在
883
- * 设置卡片切换「默认展开」开关后边栏立即跟随展开/收起。面板摊开时例行
884
- * 拉取(刷新 minPanelWidth 等设置)不传此 flag:只刷新配置,绝不回写
885
- * 开合状态,否则刚点开的边栏会被 defaultOpen 当场压回(点击无反应)。
898
+ * syncDefaultOpen=true(启动首次加载)时,若本次拉取确实改动了 defaultOpen
899
+ * 字段,则实时同步边栏开合 —— 用户改完「默认展开」设置后重新加载页面,边栏
900
+ * 立即按新偏好展开/收起。面板摊开时例行拉取(刷新 minPanelWidth 等设置)不传
901
+ * 此 flag:只刷新配置,绝不回写开合状态,否则刚点开的边栏会被 defaultOpen
902
+ * 当场压回(点击无反应)。
886
903
  */
887
904
  load(opts) {
888
905
  const syncDefaultOpen = !!(opts && opts.syncDefaultOpen);
@@ -2998,95 +3015,223 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
2998
3015
  onClick: () => store.toggle()
2999
3016
  }, /* @__PURE__ */ h(PanelIcon, { size: 18 }));
3000
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
+ }
3001
3123
  function ToggleRow(props) {
3002
- 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", {
3003
3125
  type: "button",
3004
3126
  className: "fs-settings-toggle",
3005
3127
  "data-on": props.value ? "true" : "false",
3006
3128
  disabled: props.disabled,
3007
3129
  "aria-pressed": props.value,
3130
+ "aria-label": props.label,
3008
3131
  onClick: () => {
3009
3132
  if (!props.disabled) props.onToggle(!props.value);
3010
3133
  }
3011
3134
  }));
3012
3135
  }
3013
3136
  function NumberRow(props) {
3014
- 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", {
3015
3138
  type: "number",
3016
3139
  className: "fs-settings-input",
3017
- value: props.value,
3140
+ value: props.text,
3018
3141
  min: props.min,
3019
3142
  max: props.max,
3020
3143
  disabled: props.disabled,
3021
- onChange: (e) => {
3022
- const v = Number(e.currentTarget.value);
3023
- if (Number.isFinite(v)) props.onChange(v);
3024
- }
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)
3025
3148
  }));
3026
3149
  }
3027
- function SettingsCard(props) {
3028
- 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) {
3029
3164
  useLang();
3030
- const snap = typeof props.useSettingsSnapshot === "function" ? props.useSettingsSnapshot((s) => s) : void 0;
3031
- if (snap === void 0 || snap.status === "unavailable") return null;
3032
- const disabled = snap.status !== "ready" || !snap.writable;
3033
- const v = snap.value ?? {};
3034
- return /* @__PURE__ */ h("li", { className: "fs-settings-card" + (open ? " fs-open" : "") }, /* @__PURE__ */ h("button", {
3035
- type: "button",
3036
- className: "fs-settings-head",
3037
- "aria-expanded": open,
3038
- onClick: () => {
3039
- setOpen(!open);
3040
- }
3041
- }, /* @__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", {
3042
- className: "fs-settings-chevron",
3043
- width: "14",
3044
- height: "14",
3045
- viewBox: "0 0 14 14",
3046
- fill: "none"
3047
- }, /* @__PURE__ */ h("path", {
3048
- d: "M3 5L7 9L11 5",
3049
- stroke: "currentColor",
3050
- strokeWidth: "1.5",
3051
- strokeLinecap: "round",
3052
- strokeLinejoin: "round"
3053
- }))), open ? /* @__PURE__ */ h("div", { className: "fs-settings-body" }, /* @__PURE__ */ h(ToggleRow, {
3054
- label: t("settingsAutoRefresh"),
3055
- value: !!v.autoRefresh,
3056
- disabled,
3057
- onToggle: (val) => {
3058
- props.setField?.("autoRefresh", val);
3059
- }
3060
- }), /* @__PURE__ */ h(ToggleRow, {
3061
- label: t("settingsDefaultOpen"),
3062
- value: !!v.defaultOpen,
3063
- disabled,
3064
- onToggle: (val) => {
3065
- props.setField?.("defaultOpen", val);
3066
- }
3067
- }), /* @__PURE__ */ h(NumberRow, {
3068
- label: t("settingsMinWidth"),
3069
- value: v.minPanelWidth ?? 20,
3070
- min: 15,
3071
- 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",
3072
3216
  disabled,
3073
- onChange: (val) => {
3074
- props.setField?.("minPanelWidth", val);
3075
- }
3076
- }), /* @__PURE__ */ h(NumberRow, {
3077
- label: t("settingsFontSize"),
3078
- value: v.contentFontSize ?? 13,
3079
- min: 11,
3080
- 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,
3081
3224
  disabled,
3082
- onChange: (val) => {
3083
- props.setField?.("contentFontSize", val);
3084
- }
3085
- })) : 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));
3086
3232
  }
3087
3233
  //#endregion
3088
3234
  //#region src/client/index.tsx
3089
- const SETTINGS_NAMESPACE = "dsh-flyout-sidebar";
3090
3235
  window.__ModuleLoader__.load({
3091
3236
  id: "dsh-flyout-sidebar",
3092
3237
  factory: (require) => {
@@ -3110,54 +3255,18 @@ body[data-ds-dark-theme] .tok-property { color: #ced4da; }
3110
3255
  order: 50,
3111
3256
  label: "Artifacts Panel"
3112
3257
  }, () => /* @__PURE__ */ h(ArtifactsPanel, null)));
3113
- settingsStore.load({ syncDefaultOpen: true });
3114
- ctx.inject(["settingsScope"], (sctx) => {
3115
- const binder = sctx.get("settingsScope");
3116
- if (!binder) return;
3117
- const scope = binder.bind({ namespace: SETTINGS_NAMESPACE });
3118
- const scopedSlots = sctx.get("slots");
3119
- if (!scopedSlots) return;
3120
- let localSnap = scope.getSnapshot();
3121
- const localListeners = /* @__PURE__ */ new Set();
3122
- const publishLocal = (next) => {
3123
- if (next === localSnap) return;
3124
- localSnap = next;
3125
- for (const fn of localListeners) try {
3126
- fn();
3127
- } catch {}
3128
- };
3129
- sctx.effect(() => scope.subscribe(() => {
3130
- publishLocal(scope.getSnapshot());
3131
- settingsStore.load({ syncDefaultOpen: true });
3132
- }), "flyout: settings scope");
3133
- const useSettingsSnapshot = (selector) => React.useSyncExternalStore((cb) => {
3134
- localListeners.add(cb);
3135
- return () => {
3136
- localListeners.delete(cb);
3137
- };
3138
- }, () => selector(localSnap));
3139
- const setField = (field, value) => {
3140
- const cur = localSnap;
3141
- const nextValue = cur.value && typeof cur.value === "object" ? {
3142
- ...cur.value,
3143
- [field]: value
3144
- } : { [field]: value };
3145
- publishLocal({
3146
- ...cur,
3147
- value: nextValue
3148
- });
3149
- scope.set(field, value);
3150
- };
3151
- scopedSlots.inject("settings.plugin.item", () => scopedSlots.register({
3152
- name: "settings.plugin.item",
3153
- key: SETTINGS_NAMESPACE,
3154
- locale: SETTINGS_NAMESPACE,
3155
- inject: () => ({
3156
- useSettingsSnapshot,
3157
- setField
3158
- })
3159
- }, (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");
3160
3268
  });
3269
+ settingsStore.load({ syncDefaultOpen: true });
3161
3270
  }
3162
3271
  };
3163
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.9",
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",