dshmarket 1.51.0 → 1.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/client.js CHANGED
@@ -374,6 +374,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
374
374
  stateLive: "已生效",
375
375
  stateRestart: "已安装,重启后生效",
376
376
  stateInert: "已安装,未生效",
377
+ stateDependencyLibrary: "{0} 的依赖库",
377
378
  stateBroken: "已安装,校验未通过",
378
379
  stateDisabled: "已停用",
379
380
  phaseResolving: "解析依赖",
@@ -909,6 +910,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
909
910
  stateLive: "Active",
910
911
  stateRestart: "Installed — restart to apply",
911
912
  stateInert: "Installed, not active",
913
+ stateDependencyLibrary: "Library of {0}",
912
914
  stateBroken: "Installed, verification failed",
913
915
  stateDisabled: "Disabled",
914
916
  phaseResolving: "Resolving dependencies",
@@ -2690,6 +2692,78 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
2690
2692
  "warnLine": "nUhMVa_warnLine"
2691
2693
  };
2692
2694
  //#endregion
2695
+ //#region src/client/market-mark.ts
2696
+ /**
2697
+ * The market's block mark as geometry rather than as a component.
2698
+ *
2699
+ * Two consumers draw this mark and they must not drift:
2700
+ *
2701
+ * - `MarketLogo` (MarketSection.tsx) renders it inside the section as an
2702
+ * ordinary React SVG in `currentColor`, including the animated variant;
2703
+ * - `settings-nav-icon.ts` serialises it into a CSS mask for the settings
2704
+ * navigation glyph, which cannot use `currentColor` (a mask is an
2705
+ * independent image) and so needs it as standalone markup.
2706
+ *
2707
+ * Keeping the numbers here means a change to the mark is one edit, and the
2708
+ * suite can hold both renderings to the same source.
2709
+ *
2710
+ * The mark is the brand asset in `assets/logo.svg`: an 8-cell grid plus the
2711
+ * block being plugged into its empty corner, offset and tilted 9°.
2712
+ */
2713
+ /** Side of one block, and its corner radius. */
2714
+ const MARK_BLOCK_SIZE = 3.3;
2715
+ const MARK_BLOCK_RADIUS = .53;
2716
+ /** The eight grid cells, row-major. The ninth slot stays empty on purpose. */
2717
+ const MARK_GRID_BLOCKS = [
2718
+ {
2719
+ x: 1.96,
2720
+ y: 3.36
2721
+ },
2722
+ {
2723
+ x: 5.71,
2724
+ y: 3.36
2725
+ },
2726
+ {
2727
+ x: 1.96,
2728
+ y: 7.11
2729
+ },
2730
+ {
2731
+ x: 5.71,
2732
+ y: 7.11
2733
+ },
2734
+ {
2735
+ x: 9.46,
2736
+ y: 7.11
2737
+ },
2738
+ {
2739
+ x: 1.96,
2740
+ y: 10.86
2741
+ },
2742
+ {
2743
+ x: 5.71,
2744
+ y: 10.86
2745
+ },
2746
+ {
2747
+ x: 9.46,
2748
+ y: 10.86
2749
+ }
2750
+ ];
2751
+ /**
2752
+ * The block being plugged in: OUTSIDE the grid's empty corner, offset
2753
+ * (+1.28, -1.27) and tilted 9deg, exactly as in assets/logo.svg. The earlier
2754
+ * icon sat it neatly in the empty slot, which reads as one crooked tile
2755
+ * rather than a block arriving — the whole idea of the mark, and the reason
2756
+ * it no longer matched the GitHub logo.
2757
+ */
2758
+ const MARK_PLUG_BLOCK = {
2759
+ x: 10.74,
2760
+ y: 2.09,
2761
+ degrees: 9,
2762
+ /** Rotation origin: the plug block's own centre. */
2763
+ originX: 12.39,
2764
+ originY: 3.74
2765
+ };
2766
+ //#endregion
2693
2767
  //#region src/client/comments.ts
2694
2768
  /**
2695
2769
  * giscus wiring for the in-market comment thread.
@@ -2863,6 +2937,64 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
2863
2937
  ]
2864
2938
  });
2865
2939
  }
2940
+ /** Keep keystrokes out of the market's large render tree, not just its filter.
2941
+ * Only settled queries reach the parent. Explicit navigation remains immediate.
2942
+ */
2943
+ function SearchInput({ value, onCommit, className, placeholder, resetToken }) {
2944
+ const [draft, setDraft] = (0, react.useState)(value);
2945
+ const timer = (0, react.useRef)(null);
2946
+ const composing = (0, react.useRef)(false);
2947
+ const cancel = (0, react.useCallback)(() => {
2948
+ if (timer.current !== null) clearTimeout(timer.current);
2949
+ timer.current = null;
2950
+ }, []);
2951
+ const commit = (next) => {
2952
+ cancel();
2953
+ onCommit(next);
2954
+ };
2955
+ const schedule = (next) => {
2956
+ cancel();
2957
+ if (composing.current) return;
2958
+ if (next === "") commit(next);
2959
+ else timer.current = setTimeout(() => commit(next), 250);
2960
+ };
2961
+ (0, react.useLayoutEffect)(() => {
2962
+ cancel();
2963
+ setDraft(value);
2964
+ }, [
2965
+ value,
2966
+ resetToken,
2967
+ cancel
2968
+ ]);
2969
+ (0, react.useEffect)(() => cancel, [cancel]);
2970
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
2971
+ className,
2972
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 14 }),
2973
+ placeholder,
2974
+ value: draft,
2975
+ onChange: (event) => {
2976
+ const next = event.target.value;
2977
+ setDraft(next);
2978
+ schedule(next);
2979
+ },
2980
+ onCompositionStart: () => {
2981
+ composing.current = true;
2982
+ cancel();
2983
+ },
2984
+ onCompositionEnd: (event) => {
2985
+ composing.current = false;
2986
+ const next = event.currentTarget.value;
2987
+ setDraft(next);
2988
+ schedule(next);
2989
+ },
2990
+ onKeyDown: (event) => {
2991
+ if (event.key === "Enter" && !composing.current && !event.nativeEvent.isComposing && event.keyCode !== 229) commit(event.currentTarget.value);
2992
+ },
2993
+ onBlur: (event) => {
2994
+ if (!composing.current) commit(event.currentTarget.value);
2995
+ }
2996
+ });
2997
+ }
2866
2998
  //#endregion
2867
2999
  //#region src/client/operations.ts
2868
3000
  const BUCKETS = {
@@ -5191,7 +5323,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
5191
5323
  });
5192
5324
  }
5193
5325
  /** The state label + dot for one activation result (P0-2). */
5194
- function activationMeta(state, t) {
5326
+ function activationMeta(state, t, dependencyOf) {
5327
+ if (state === "inert" && dependencyOf !== void 0) return {
5328
+ label: t("stateDependencyLibrary").replace("{0}", dependencyOf),
5329
+ dot: "done"
5330
+ };
5195
5331
  if (state === "live") return {
5196
5332
  label: t("stateLive"),
5197
5333
  dot: "done"
@@ -6073,86 +6209,36 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
6073
6209
  * Official-style market glyph: the shared block-grid brand mark converted to
6074
6210
  * the official monochrome icon form (16×16, fill="currentColor") so it
6075
6211
  * follows the active theme. Mirrors the settings-nav glyph used for the
6076
- * "market" section id.
6212
+ * "market" section id — both now draw the geometry in market-mark.ts, so the
6213
+ * nav entry and the section it opens cannot drift apart.
6077
6214
  */
6078
6215
  function MarketLogo({ size = 16, style, animated = false }) {
6079
6216
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
6080
6217
  width: size,
6081
6218
  height: size,
6082
- viewBox: "0 0 16 16",
6219
+ viewBox: `0 0 16 16`,
6083
6220
  fill: "none",
6084
6221
  xmlns: "http://www.w3.org/2000/svg",
6085
6222
  "aria-hidden": "true",
6086
6223
  style,
6087
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
6224
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
6088
6225
  fill: "currentColor",
6089
- children: [
6090
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6091
- x: "1.96",
6092
- y: "3.36",
6093
- width: "3.3",
6094
- height: "3.3",
6095
- rx: "0.53"
6096
- }),
6097
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6098
- x: "5.71",
6099
- y: "3.36",
6100
- width: "3.3",
6101
- height: "3.3",
6102
- rx: "0.53"
6103
- }),
6104
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6105
- x: "1.96",
6106
- y: "7.11",
6107
- width: "3.3",
6108
- height: "3.3",
6109
- rx: "0.53"
6110
- }),
6111
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6112
- x: "5.71",
6113
- y: "7.11",
6114
- width: "3.3",
6115
- height: "3.3",
6116
- rx: "0.53"
6117
- }),
6118
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6119
- x: "9.46",
6120
- y: "7.11",
6121
- width: "3.3",
6122
- height: "3.3",
6123
- rx: "0.53"
6124
- }),
6125
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6126
- x: "1.96",
6127
- y: "10.86",
6128
- width: "3.3",
6129
- height: "3.3",
6130
- rx: "0.53"
6131
- }),
6132
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6133
- x: "5.71",
6134
- y: "10.86",
6135
- width: "3.3",
6136
- height: "3.3",
6137
- rx: "0.53"
6138
- }),
6139
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6140
- x: "9.46",
6141
- y: "10.86",
6142
- width: "3.3",
6143
- height: "3.3",
6144
- rx: "0.53"
6145
- })
6146
- ]
6226
+ children: MARK_GRID_BLOCKS.map((block) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6227
+ x: block.x,
6228
+ y: block.y,
6229
+ width: MARK_BLOCK_SIZE,
6230
+ height: MARK_BLOCK_SIZE,
6231
+ rx: MARK_BLOCK_RADIUS
6232
+ }, `${block.x},${block.y}`))
6147
6233
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6148
6234
  className: animated ? Market_module_css_default.logoPlug : void 0,
6149
- x: "10.74",
6150
- y: "2.09",
6151
- width: "3.3",
6152
- height: "3.3",
6153
- rx: "0.53",
6235
+ x: MARK_PLUG_BLOCK.x,
6236
+ y: MARK_PLUG_BLOCK.y,
6237
+ width: MARK_BLOCK_SIZE,
6238
+ height: MARK_BLOCK_SIZE,
6239
+ rx: MARK_BLOCK_RADIUS,
6154
6240
  fill: "currentColor",
6155
- transform: animated ? void 0 : "rotate(9 12.39 3.74)"
6241
+ transform: animated ? void 0 : `rotate(${MARK_PLUG_BLOCK.degrees} ${MARK_PLUG_BLOCK.originX} ${MARK_PLUG_BLOCK.originY})`
6156
6242
  })]
6157
6243
  });
6158
6244
  }
@@ -6363,6 +6449,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
6363
6449
  return saved || "discover";
6364
6450
  });
6365
6451
  const [q, setQ] = (0, react.useState)("");
6452
+ const [discoverSearchReset, resetDiscoverSearch] = (0, react.useState)(0);
6453
+ const [installedSearchReset, resetInstalledSearch] = (0, react.useState)(0);
6366
6454
  /** Per-tab searches stay independent: discover / themes / installed. */
6367
6455
  const [qThemes, setQThemes] = (0, react.useState)("");
6368
6456
  const [qFavorites, setQFavorites] = (0, react.useState)("");
@@ -6377,10 +6465,12 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
6377
6465
  if (kind === "installed") {
6378
6466
  setTab("installed");
6379
6467
  setQInstalled(value);
6468
+ resetInstalledSearch((n) => n + 1);
6380
6469
  } else if (kind === "discover") {
6381
6470
  setTab("discover");
6382
6471
  setCat("all");
6383
6472
  setQ(value);
6473
+ resetDiscoverSearch((n) => n + 1);
6384
6474
  }
6385
6475
  }, [props.preferredSubsectionId]);
6386
6476
  const [confirming, setConfirming] = (0, react.useState)(null);
@@ -9097,7 +9187,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9097
9187
  children: activationWarnings.map(({ name, info }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
9098
9188
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: name }),
9099
9189
  " — ",
9100
- activationMeta(info.state, t).label,
9190
+ activationMeta(info.state, t, info.dependencyOf).label,
9101
9191
  info.reasons.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
9102
9192
  className: Market_module_css_default.spec,
9103
9193
  children: [
@@ -9476,13 +9566,13 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9476
9566
  className: Market_module_css_default.stickyHead,
9477
9567
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9478
9568
  className: Market_module_css_default.tabSearchRow,
9479
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
9569
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchInput, {
9570
+ resetToken: discoverSearchReset,
9480
9571
  className: Market_module_css_default.tabSearch,
9481
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 14 }),
9482
9572
  placeholder: t("searchPh"),
9483
9573
  value: q,
9484
- onChange: (e) => setQ(e.target.value)
9485
- })
9574
+ onCommit: setQ
9575
+ }, "discover")
9486
9576
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9487
9577
  className: Market_module_css_default.cats,
9488
9578
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -9567,13 +9657,12 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9567
9657
  children: t("favoritesEmpty")
9568
9658
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9569
9659
  className: Market_module_css_default.themeToolbar,
9570
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
9660
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchInput, {
9571
9661
  className: Market_module_css_default.themeSearch,
9572
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 14 }),
9573
9662
  placeholder: t("searchFavoritesPh"),
9574
9663
  value: qFavorites,
9575
- onChange: (e) => setQFavorites(e.target.value)
9576
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9664
+ onCommit: setQFavorites
9665
+ }, "favorites"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9577
9666
  className: Market_module_css_default.themeToolbarActions,
9578
9667
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilterMenu, {
9579
9668
  sortField: favSortField,
@@ -9654,13 +9743,12 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9654
9743
  ] })] }) : tab === "themes" && themeSnap !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
9655
9744
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9656
9745
  className: Market_module_css_default.themeToolbar,
9657
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
9746
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchInput, {
9658
9747
  className: Market_module_css_default.themeSearch,
9659
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 14 }),
9660
9748
  placeholder: t("searchPh"),
9661
9749
  value: qThemes,
9662
- onChange: (e) => setQThemes(e.target.value)
9663
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9750
+ onCommit: setQThemes
9751
+ }, "themes"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9664
9752
  className: Market_module_css_default.themeToolbarActions,
9665
9753
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilterMenu, {
9666
9754
  sortField: themeSortField,
@@ -9742,13 +9830,13 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9742
9830
  }),
9743
9831
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9744
9832
  className: Market_module_css_default.tabSearchRow,
9745
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
9833
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchInput, {
9834
+ resetToken: installedSearchReset,
9746
9835
  className: Market_module_css_default.tabSearch,
9747
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 14 }),
9748
9836
  placeholder: t("searchPh"),
9749
9837
  value: qInstalled,
9750
- onChange: (e) => setQInstalled(e.target.value)
9751
- })
9838
+ onCommit: setQInstalled
9839
+ }, "installed")
9752
9840
  }),
9753
9841
  installedView === "groups" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
9754
9842
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -10067,7 +10155,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
10067
10155
  const generation = status?.kind === "generation" || isGenerationSpec(String(spec));
10068
10156
  const localDev = !generation && (/^(?:link|file):/i.test(String(spec)) || status?.kind === "linked");
10069
10157
  const act = activations[name];
10070
- const meta = act !== void 0 ? activationMeta(act.state, t) : null;
10158
+ const meta = act !== void 0 ? activationMeta(act.state, t, act.dependencyOf) : null;
10071
10159
  const version = status && status.version ? "v" + status.version : "";
10072
10160
  const specText = String(spec);
10073
10161
  const specRedundant = version !== "" && /^[\^~]?\d/.test(specText);
@@ -11417,6 +11505,141 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11417
11505
  }, (0, react.createElement)("div", { className: Market_module_css_default.setHeadText }, (0, react.createElement)("div", { className: Market_module_css_default.setName }, t("nav"), version !== null ? (0, react.createElement)("span", { className: Market_module_css_default.version }, ` v${version}`) : null, prerelease ? (0, react.createElement)("span", { className: Market_module_css_default.setBetaTag }, t("setChannelBeta")) : null), (0, react.createElement)("div", { className: Market_module_css_default.setDesc }, t("setCardDesc"))), (0, react.createElement)("span", { className: open ? `${Market_module_css_default.setChevron} ${Market_module_css_default.setChevronOpen}` : Market_module_css_default.setChevron }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 }))), open ? (0, react.createElement)("div", { className: Market_module_css_default.setBody }, body) : null);
11418
11506
  }
11419
11507
  //#endregion
11508
+ //#region src/client/settings-nav-icon.ts
11509
+ /**
11510
+ * The market's block mark in the settings navigation.
11511
+ *
11512
+ * The settings shell picks nav glyphs from a closed list of section ids
11513
+ * (`models`, `agent-presets`, `plugins`) and falls back to its own gear for
11514
+ * every other id; `settings.section` projects only `id` / `order` / `label`,
11515
+ * so a registrant has no icon to pass — the slot contract in
11516
+ * `@deepseek-ai/dsh-client-ui-settings` and the runtime slot inventory both
11517
+ * list exactly those three options. Every third-party section therefore wears
11518
+ * the gear, the market included.
11519
+ *
11520
+ * So the market claims its own row once the dialog is mounted and swaps the
11521
+ * fallback gear for the block mark — the same mark `MarketLogo` draws inside
11522
+ * the section (see market-mark.ts), which is what makes the nav entry read as
11523
+ * the same thing as the page it opens. `dsh-better-sidebar` and
11524
+ * `dsh-skill-mcp-panel` solve it the same way.
11525
+ *
11526
+ * Scope, deliberately narrow:
11527
+ *
11528
+ * - only the row whose visible text equals this plugin's own localized
11529
+ * section label is marked; no shell structure is touched;
11530
+ * - the marker and the injected stylesheet belong to a `ctx.effect`, so they
11531
+ * are removed with the fiber;
11532
+ * - a locale switch re-claims the row through the MutationObserver, so the
11533
+ * label and the glyph never disagree.
11534
+ *
11535
+ * Delete this module (and its call in index.ts) the day `settings.section`
11536
+ * grows an `icon` field.
11537
+ */
11538
+ /** Marks the one nav row this plugin owns. */
11539
+ const NAV_ICON_MARKER = "data-dsh-market-nav-icon";
11540
+ /**
11541
+ * The nav rows of the settings dialog. The shell renders each
11542
+ * `settings.section` entry as a `<button>` inside the panel's `<nav>`
11543
+ * (SettingsPanel in dsh-client-ui-settings-general).
11544
+ */
11545
+ const NAV_ROW_SELECTOR = "[role=\"dialog\"] nav button";
11546
+ /**
11547
+ * The mark as standalone SVG for a CSS `mask-image`.
11548
+ *
11549
+ * Painted pure black on purpose: a mask reads alpha only, and the visible
11550
+ * colour comes from the element's `background-color: currentColor`. The
11551
+ * plug block carries its tilt without the animated variant's transform
11552
+ * classes — a mask cannot animate through CSS-module classes.
11553
+ */
11554
+ function marketMaskSvg() {
11555
+ const blocks = MARK_GRID_BLOCKS.map((block) => `<rect x="${block.x}" y="${block.y}" width="${MARK_BLOCK_SIZE}" height="${MARK_BLOCK_SIZE}" rx="${MARK_BLOCK_RADIUS}"/>`).join("");
11556
+ const plug = MARK_PLUG_BLOCK;
11557
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#000"><g>${blocks}</g><rect x="${plug.x}" y="${plug.y}" width="${MARK_BLOCK_SIZE}" height="${MARK_BLOCK_SIZE}" rx="${MARK_BLOCK_RADIUS}" transform="rotate(${plug.degrees} ${plug.originX} ${plug.originY})"/></svg>`;
11558
+ }
11559
+ /** The mask URL for the mark (encoded at runtime, never hand-escaped). */
11560
+ function marketMaskUrl(svg = marketMaskSvg()) {
11561
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
11562
+ }
11563
+ /**
11564
+ * Whether a nav row is this plugin's own.
11565
+ *
11566
+ * Pure, and the only decision this feature makes: the row whose visible text
11567
+ * is the section label the shell is currently projecting. An empty label
11568
+ * matches nothing — a locale that has not resolved yet must not mark the
11569
+ * whole nav.
11570
+ */
11571
+ function isOwnNavRow(rowText, wantedLabel) {
11572
+ const wanted = String(wantedLabel ?? "").trim();
11573
+ if (wanted.length === 0) return false;
11574
+ return String(rowText ?? "").trim() === wanted;
11575
+ }
11576
+ /** Stylesheet for the marked row: hide the shell's gear, draw the mark. */
11577
+ function navIconCss(maskUrl) {
11578
+ return [
11579
+ `[${NAV_ICON_MARKER}] > svg { display: none; }`,
11580
+ `[${NAV_ICON_MARKER}]::before {`,
11581
+ ` content: '';`,
11582
+ ` flex: none;`,
11583
+ ` width: 16px;`,
11584
+ ` height: 16px;`,
11585
+ ` background-color: currentColor;`,
11586
+ ` -webkit-mask-image: url("${maskUrl}");`,
11587
+ ` mask-image: url("${maskUrl}");`,
11588
+ ` -webkit-mask-repeat: no-repeat;`,
11589
+ ` mask-repeat: no-repeat;`,
11590
+ ` -webkit-mask-position: center;`,
11591
+ ` mask-position: center;`,
11592
+ ` -webkit-mask-size: 16px 16px;`,
11593
+ ` mask-size: 16px 16px;`,
11594
+ `}`
11595
+ ].join("\n");
11596
+ }
11597
+ /**
11598
+ * Install the nav glyph.
11599
+ *
11600
+ * @param ctx - client context, for effect ownership.
11601
+ * @param resolveLabel - this plugin's current section label (the same thunk
11602
+ * the `settings.section` registration passes), re-read on every sync so a
11603
+ * locale switch is picked up without re-registering.
11604
+ */
11605
+ function installSettingsNavIcon(ctx, resolveLabel) {
11606
+ if (typeof document === "undefined") return;
11607
+ ctx.effect(() => {
11608
+ const tag = document.createElement("style");
11609
+ tag.dataset.plugin = "dshmarket";
11610
+ tag.dataset.pluginCss = "dshmarket/settings-nav-icon";
11611
+ tag.textContent = navIconCss(marketMaskUrl());
11612
+ document.head.appendChild(tag);
11613
+ let disposed = false;
11614
+ let scheduled = false;
11615
+ const sync = () => {
11616
+ scheduled = false;
11617
+ if (disposed) return;
11618
+ const wanted = resolveLabel();
11619
+ for (const row of document.querySelectorAll(NAV_ROW_SELECTOR)) if (isOwnNavRow(row.textContent, wanted)) row.setAttribute(NAV_ICON_MARKER, "");
11620
+ else row.removeAttribute(NAV_ICON_MARKER);
11621
+ };
11622
+ const schedule = () => {
11623
+ if (scheduled || disposed) return;
11624
+ scheduled = true;
11625
+ queueMicrotask(sync);
11626
+ };
11627
+ sync();
11628
+ const observer = new MutationObserver(schedule);
11629
+ observer.observe(document.body, {
11630
+ childList: true,
11631
+ subtree: true,
11632
+ characterData: true
11633
+ });
11634
+ return () => {
11635
+ disposed = true;
11636
+ observer.disconnect();
11637
+ for (const row of document.querySelectorAll(`[${NAV_ICON_MARKER}]`)) row.removeAttribute(NAV_ICON_MARKER);
11638
+ tag.remove();
11639
+ };
11640
+ }, "dsh-market: settings nav icon");
11641
+ }
11642
+ //#endregion
11420
11643
  //#region src/client/index.ts
11421
11644
  /**
11422
11645
  * dsh-market client: registers a "Market" settings section rendering the
@@ -11459,6 +11682,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
11459
11682
  en
11460
11683
  }), "dsh-market: dictionaries");
11461
11684
  const t = ctx.locale.bind(NS);
11685
+ installSettingsNavIcon(ctx, () => t("nav"));
11462
11686
  /**
11463
11687
  * The market's own panel, as an element — one builder for the settings
11464
11688
  * section this package registers and for `market.render()` (#602). Built
package/lib/http.js CHANGED
@@ -11,11 +11,43 @@ export function sendJson(response, status, payload) {
11
11
  });
12
12
  response.end(JSON.stringify(payload));
13
13
  }
14
- /** True when the request's Origin matches its Host — required on every POST route. */
14
+ /**
15
+ * True when the request's Origin, IF IT HAS ONE, matches its Host — required
16
+ * on every POST route.
17
+ *
18
+ * An absent Origin is allowed (#648). The check exists to stop another web
19
+ * page from driving this loopback API, and the Fetch spec has browsers send
20
+ * `Origin` on every POST — including same-origin and cross-origin form
21
+ * submissions. A request that arrives WITHOUT it therefore did not come from
22
+ * a page at all, and a non-browser client can set any Origin it likes, so
23
+ * refusing the header's absence bought nothing while breaking a whole
24
+ * supported host: the Desktop app's own proxy strips `origin`, `host`,
25
+ * `cookie` and `sec-fetch-site` before forwarding to the in-process host and
26
+ * injects a cookie only that host can sign. Every mutating request from the
27
+ * Desktop build was answered 403 `untrusted origin`.
28
+ *
29
+ * What still holds after the change, and is the part worth keeping:
30
+ *
31
+ * - a cross-site page is refused — its Origin is present and different;
32
+ * - `Origin: null` (a sandboxed iframe, a `data:` document) is refused, because
33
+ * `null` is a present-but-unparseable origin, not an absent one;
34
+ * - a same-host-but-different-port request is refused;
35
+ * - an EMPTY Origin is refused too: the header being present and unparseable
36
+ * is not the same statement as its being absent, and only absence is what
37
+ * a stripping proxy produces.
38
+ *
39
+ * The header's absence is a statement about the client, not about intent, and
40
+ * the intent is the only thing this can judge.
41
+ *
42
+ * @param request - the incoming request.
43
+ * @returns whether the request may mutate market state.
44
+ */
15
45
  export function sameOrigin(request) {
16
46
  const origin = request.headers.origin;
47
+ if (origin === undefined)
48
+ return true;
17
49
  const host = request.headers.host;
18
- if (origin === undefined || host === undefined)
50
+ if (host === undefined)
19
51
  return false;
20
52
  try {
21
53
  return new URL(origin).host === host;
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * composes the webServer and shell services.
4
4
  */
5
5
  import { createDesktopPluginRuntime } from './dsh-cli.js';
6
+ import { isDshProfileName } from './profile.js';
6
7
  import { mountMarketRoutes } from './routes.js';
7
8
  import { installDesktopMarketSettings, installMarketSettings } from './settings.js';
8
9
  export const name = 'dsh-market';
@@ -23,6 +24,30 @@ function argvProfile() {
23
24
  return argv[flag + 1];
24
25
  return undefined;
25
26
  }
27
+ /**
28
+ * The profile this process was launched into, as the launcher itself reports
29
+ * it. `argvProfile` only sees a `--profile` flag on this process's argv, and
30
+ * the official desktop host starts a profile through the launcher's node
31
+ * entry rather than the CLI — no flag, no `desktopProfiles` service either,
32
+ * so the market fell back to `web` and every install, update and uninstall
33
+ * landed in the web profile instead of the one the user was looking at
34
+ * (#639).
35
+ *
36
+ * The launcher's `dir` is taken with the name rather than derived from it:
37
+ * the launcher owns where a profile lives, and deriving the path would
38
+ * disagree with it for any profile that does not sit in the default place.
39
+ * A name the market could not use as a directory segment is refused rather
40
+ * than joined into a path.
41
+ */
42
+ function launchedProfile(context) {
43
+ if (context === undefined)
44
+ return undefined;
45
+ const name = typeof context.name === 'string' ? context.name.trim() : '';
46
+ const dir = typeof context.dir === 'string' ? context.dir.trim() : '';
47
+ if (!isDshProfileName(name) || dir === '')
48
+ return undefined;
49
+ return { name, dir };
50
+ }
26
51
  /**
27
52
  * Resolve the host's `agents` inventory lazily — at request time, not at
28
53
  * market startup, so the guard sees whichever agents exist by the time an
@@ -37,8 +62,15 @@ export function apply(ctx, config) {
37
62
  const host = hostCtx;
38
63
  const desktopProfiles = ctx.get('desktopProfiles');
39
64
  if (desktopProfiles === undefined) {
65
+ // An explicit `profile:` in cordis.yml is the operator speaking and
66
+ // still wins; the launcher's own answer comes next, and only then the
67
+ // flag-and-default guesswork. The launcher's directory rides along with
68
+ // its name, and never with somebody else's.
69
+ const launched = launchedProfile(ctx.get('profileContext'));
70
+ const useLaunchedDir = config?.profile === undefined && launched !== undefined;
40
71
  const resolved = {
41
- profile: config?.profile ?? argvProfile() ?? 'web',
72
+ profile: config?.profile ?? launched?.name ?? argvProfile() ?? 'web',
73
+ ...(useLaunchedDir ? { profileDirectory: launched.dir } : {}),
42
74
  // Left UNDEFINED when unconfigured, deliberately: `?? true` here
43
75
  // would turn "the operator said nothing" into "the operator said
44
76
  // yes", and restartAllowed() could no longer tell them apart — which
package/lib/profile.js CHANGED
@@ -78,6 +78,39 @@ export function readManifestDeps(profile, explicitDir) {
78
78
  return {};
79
79
  }
80
80
  }
81
+ /**
82
+ * For each installed package, the OTHER installed package that declares it —
83
+ * as a dependency or a peer dependency — in its own manifest.
84
+ *
85
+ * pnpm's auto-install-peers writes a plugin's peers into the profile manifest
86
+ * as direct dependencies, so a native binding a plugin needs shows up in the
87
+ * installed list looking exactly like a plugin the user chose (#634). What
88
+ * separates them is that somebody else asked for it.
89
+ *
90
+ * Ownership is decided by the first owner in sorted order, so the answer does
91
+ * not depend on the manifest's key order. A package that declares itself is
92
+ * ignored, and so is a cycle's other half once one owner is chosen.
93
+ */
94
+ export function readDependencyOwners(profile, names, explicitDir) {
95
+ const installed = new Set(names);
96
+ const owners = {};
97
+ for (const owner of [...names].sort()) {
98
+ const manifest = readInstalledManifest(profile, owner, explicitDir);
99
+ if (typeof manifest !== 'object' || manifest === null)
100
+ continue;
101
+ const declared = manifest;
102
+ for (const field of [declared.dependencies, declared.peerDependencies]) {
103
+ if (typeof field !== 'object' || field === null)
104
+ continue;
105
+ for (const dependency of Object.keys(field)) {
106
+ if (dependency === owner || !installed.has(dependency))
107
+ continue;
108
+ owners[dependency] ??= owner;
109
+ }
110
+ }
111
+ }
112
+ return owners;
113
+ }
81
114
  function objectRecord(value) {
82
115
  return typeof value === 'object' && value !== null && !Array.isArray(value)
83
116
  ? value