@fonlan/dsh-quick-commands 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.
package/lib/client.js CHANGED
@@ -825,7 +825,7 @@ window.__ModuleLoader__.load({
825
825
  });
826
826
  }
827
827
  //#endregion
828
- //#region \0dsh-css:/Users/fonlan/Repos/dsh-quick-commands/src/client/settings-section.css.mjs
828
+ //#region \0dsh-css:/home/runner/work/dsh-quick-commands/dsh-quick-commands/src/client/settings-section.css.mjs
829
829
  const css$1 = "/**\n * Styles for the quick-commands settings page.\n *\n * The page occupies the settings panel content column: a static header\n * (title plus a one-line summary) over the always-visible command editor.\n * Inner controls follow the built-in settings language.\n */\n\n/* page layout */\n\n.qc-page {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 720px;\n}\n.qc-page-head {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.qc-page-title {\n margin: 0;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n.qc-page-sub {\n margin: 0;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n font-size: 13px;\n line-height: 1.5;\n}\n\n/* body: workspace / command-set editor */\n\n.qc-settings-body {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.qc-settings-intro {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n margin: 0;\n line-height: 1.5;\n}\n.qc-settings-anchor {\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 12px;\n}\n.qc-settings-anchor-label {\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-weight: 500;\n}\n.qc-settings-radio {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n cursor: pointer;\n}\n.qc-settings-error {\n font-size: 12px;\n color: var(--dsw-alias-state-error-primary, #e5484d);\n}\n.qc-settings-empty {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n margin: 0;\n}\n.qc-settings-workspace {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 10px;\n padding: 10px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.qc-settings-workspace-head {\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n.qc-settings-workspace-title {\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n}\n.qc-settings-workspace-path {\n font-size: 11px;\n font-family: var(--dsw-font-family-mono, monospace);\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.qc-settings-no-commands {\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n margin: 0;\n}\n.qc-settings-command {\n display: flex;\n gap: 6px;\n align-items: center;\n}\n.qc-settings-field {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1, #2a2a2d);\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-size: 12px;\n padding: 6px 8px;\n outline: none;\n}\n.qc-settings-field::placeholder {\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n}\n.qc-settings-field:focus {\n border-color: var(--dsw-alias-brand-primary, #4a9eff);\n}\n.qc-settings-field-name {\n width: 160px;\n flex: none;\n}\n.qc-settings-field-command {\n flex: 1;\n font-family: var(--dsw-font-family-mono, monospace);\n}\n.qc-settings-icon-btn {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n cursor: pointer;\n padding: 4px;\n display: inline-flex;\n align-items: center;\n border-radius: 6px;\n}\n.qc-settings-icon-btn:hover {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n color: var(--dsw-alias-state-error-primary, #e5484d);\n}\n.qc-settings-add-btn {\n align-self: flex-start;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-size: 12px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.qc-settings-add-btn:hover {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n}\n";
830
830
  const tagId$1 = "@fonlan/dsh-quick-commands/settings-section.css#d7004149";
831
831
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
@@ -1152,7 +1152,7 @@ window.__ModuleLoader__.load({
1152
1152
  cardAnchorButton: "Anchored at button"
1153
1153
  };
1154
1154
  //#endregion
1155
- //#region \0dsh-css:/Users/fonlan/Repos/dsh-quick-commands/src/client/quick-commands.css.mjs
1155
+ //#region \0dsh-css:/home/runner/work/dsh-quick-commands/dsh-quick-commands/src/client/quick-commands.css.mjs
1156
1156
  const css = "/* @fonlan/dsh-quick-commands client styles.\n Uses theme CSS variables (--dsw-alias-*); falls back to neutral literals.\n Constraint: this deployment's theme defines only these text tokens —\n label-primary, label-secondary, label-tertiary, label-dimmed. READABLE\n description copy uses label-secondary / label-tertiary (never label-dimmed,\n which is intentionally near-invisible and reserved for disabled states). */\n\n/* ── header button (icon-only, left of Session log pill) ─────────────────── */\n.qc-head-wrap {\n position: relative;\n display: inline-flex;\n align-items: center;\n}\n/* Sized to DSH built-in header buttons (e.g. the open-in-app split button on\n the left): 26px tall pill, 13px radius, hairline border-l4, hover uses the\n shared --dsw-alias-interactive-bg-hover token. */\n.qc-head-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n border: 0.5px solid var(--dsw-alias-border-l4, rgba(128, 128, 128, 0.35));\n border-radius: 13px;\n background: transparent;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n cursor: pointer;\n padding: 0;\n}\n.qc-head-btn:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent));\n}\n.qc-head-btn:disabled {\n color: var(--dsw-alias-label-dimmed, #666);\n cursor: not-allowed;\n}\n.qc-head-btn svg {\n flex: none;\n}\n\n/* ── command menu popover (anchored under the button) ────────────────────── */\n.qc-menu {\n position: absolute;\n top: calc(100% + 6px);\n right: 12px;\n min-width: 280px;\n max-height: 420px;\n overflow: auto;\n background: var(--dsw-alias-bg-overlay, #1b1b1c);\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 10px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);\n z-index: 50;\n padding: 6px;\n display: flex;\n flex-direction: column;\n}\n.qc-menu-head {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 8px 6px;\n border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n}\n.qc-menu-title {\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n}\n.qc-menu-ws {\n flex: 1;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n margin-right: 2px;\n}\n.qc-menu-remote {\n flex: 0 0 auto;\n font-size: 10px;\n font-weight: 600;\n line-height: 1;\n padding: 2px 5px;\n border-radius: 5px;\n color: var(--dsw-alias-fg-on-accent, #fff);\n background: var(--dsw-alias-accent, #4a8dff);\n letter-spacing: 0.04em;\n}\n.qc-menu-close {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n cursor: pointer;\n padding: 2px;\n display: inline-flex;\n align-items: center;\n}\n.qc-menu-empty {\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n padding: 10px 8px;\n margin: 0;\n}\n.qc-menu-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 2px;\n text-align: left;\n border: 0;\n background: transparent;\n border-radius: 8px;\n padding: 6px 8px;\n cursor: pointer;\n width: 100%;\n}\n.qc-menu-item:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n}\n.qc-menu-item:disabled {\n opacity: 0.6;\n cursor: wait;\n}\n.qc-menu-item-name {\n font-size: 13px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n}\n.qc-menu-item-cmd {\n font-family: var(--dsw-font-family-mono, monospace);\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 100%;\n}\n\n/* ── live output popup ───────────────────────────────────────────────────── */\n.qc-popup {\n position: relative; /* anchors the absolutely-positioned resize grip */\n display: flex;\n flex-direction: column;\n background: var(--dsw-alias-bg-overlay, #1b1b1c);\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 12px;\n box-shadow: 0 12px 36px rgba(0, 0, 0, 0.45);\n z-index: 60;\n overflow: hidden;\n}\n.qc-popup-corner {\n position: fixed;\n right: 20px;\n bottom: 20px;\n width: 520px;\n max-width: calc(100vw - 40px);\n height: 340px;\n max-height: calc(100vh - 120px);\n}\n.qc-popup-button {\n position: absolute;\n top: calc(100% + 6px);\n right: 12px;\n width: 440px;\n max-width: calc(100vw - 40px);\n height: 300px;\n max-height: calc(100vh - 120px);\n}\n.qc-popup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n}\n.qc-popup-title {\n flex: 1;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.qc-popup-cmd {\n font-family: var(--dsw-font-family-mono, monospace);\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 40%;\n}\n.qc-popup-remote {\n flex: 0 0 auto;\n font-size: 10px;\n font-weight: 600;\n line-height: 1;\n padding: 3px 6px;\n border-radius: 6px;\n color: var(--dsw-alias-fg-on-accent, #fff);\n background: var(--dsw-alias-accent, #4a8dff);\n letter-spacing: 0.04em;\n}\n.qc-popup-icon {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n cursor: pointer;\n padding: 4px;\n display: inline-flex;\n align-items: center;\n border-radius: 6px;\n}\n.qc-popup-icon:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n}\n.qc-popup-icon:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n.qc-popup-kill {\n color: var(--dsw-alias-state-error-primary, #e5484d);\n}\n.qc-popup-confirm {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n padding: 8px 10px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f5a623) 12%, transparent);\n color: var(--dsw-alias-label-primary, #f2f2f2);\n border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n}\n.qc-popup-btn {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n font-size: 12px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.qc-popup-btn:hover {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);\n}\n.qc-popup-btn-plain {\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n}\n.qc-popup-tabs {\n display: flex;\n gap: 2px;\n padding: 6px 10px 0;\n border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n}\n.qc-popup-tab {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n font-size: 12px;\n padding: 5px 10px;\n cursor: pointer;\n border-bottom: 2px solid transparent;\n}\n.qc-popup-tab-active {\n color: var(--dsw-alias-label-primary, #f2f2f2);\n border-bottom-color: var(--dsw-alias-brand-primary, #4a9eff);\n}\n.qc-popup-output {\n flex: 1;\n overflow: auto;\n padding: 8px 10px;\n font-family: var(--dsw-font-family-mono, monospace);\n font-size: 12px;\n line-height: 1.5;\n}\n.qc-popup-pre {\n margin: 0;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--dsw-alias-label-primary, #f2f2f2);\n}\n.qc-popup-empty {\n margin: 0;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n font-size: 12px;\n}\n.qc-popup-error {\n margin: 0 0 6px;\n color: var(--dsw-alias-state-error-primary, #e5484d);\n font-size: 12px;\n}\n.qc-popup-foot {\n display: flex;\n align-items: center;\n padding: 6px 10px;\n border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n font-size: 11px;\n}\n.qc-popup-state {\n color: var(--dsw-alias-label-secondary, #cfd3d6);\n}\n\n/* Resize handles. The popup pins its RIGHT edge (both anchors); width is\n always dragged from the LEFT border. Height is dragged from the TOP border\n (corner anchor, bottom pinned) or the BOTTOM border (button anchor, top\n pinned below the button). The free corner (top-left / bottom-left) carries\n the diagonal grip. Pointer capture lives in header.tsx; touch-action:none\n lets touch drags resize instead of scrolling. */\n.qc-popup-resize {\n position: absolute;\n z-index: 3;\n touch-action: none;\n user-select: none;\n}\n.qc-popup-resize-left {\n left: 0;\n top: 10px;\n bottom: 10px;\n width: 7px;\n cursor: ew-resize;\n}\n.qc-popup-resize-top {\n top: 0;\n left: 10px;\n right: 10px;\n height: 7px;\n cursor: ns-resize;\n}\n.qc-popup-resize-bottom {\n bottom: 0;\n left: 10px;\n right: 10px;\n height: 7px;\n cursor: ns-resize;\n}\n.qc-popup-resize-left:hover,\n.qc-popup-resize-top:hover,\n.qc-popup-resize-bottom:hover {\n background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 14%, transparent);\n}\n.qc-popup-resize-corner {\n width: 22px;\n height: 22px;\n z-index: 4;\n display: flex;\n padding: 3px;\n color: var(--dsw-alias-label-tertiary, #adb2b8);\n opacity: 0.6;\n transition: opacity .15s ease;\n}\n.qc-popup-resize-corner-tl {\n top: 0;\n left: 0;\n cursor: nwse-resize;\n align-items: flex-start;\n justify-content: flex-start;\n}\n.qc-popup-resize-corner-bl {\n bottom: 0;\n left: 0;\n cursor: nesw-resize;\n align-items: flex-end;\n justify-content: flex-start;\n}\n.qc-popup-resize-corner:hover,\n.qc-popup-resize-corner:active {\n opacity: 1;\n}\n\n/* ── mobile fallback (dsh-mobile-nav) ──────────────────────────────────────\n The dsh-mobile-nav plugin (≤1023px) hides the ENTIRE header utilities seat\n with `[data-mobile-nav=\"frame\"] [data-phase] header > :first-child >\n :last-child { display: none !important; }`, because the seat used to hold\n only the session-log-export capsule (which it relocates to the drawer\n footer). Our quick-run ▶ button rides that same seat, so it vanished on\n phones.\n\n Re-show ONLY our button: restore the seat's flex layout (out-ranking the\n host rule via `:has(.qc-head-wrap)` specificity (0,5,1) > (0,4,1)) and keep\n every other seat occupant hidden (the session-log capsule stays relocated\n to the drawer footer). Desktop (no [data-mobile-nav]) is untouched. */\n[data-mobile-nav=\"frame\"] [data-phase] header > :first-child > :last-child:has(.qc-head-wrap) {\n display: flex !important;\n margin-left: 4px !important;\n gap: 0 !important;\n}\n[data-mobile-nav=\"frame\"] [data-phase] header > :first-child > :last-child:has(.qc-head-wrap)\n [data-slot=\"conversation.session.header.utilities\"] > :not(.qc-head-wrap) {\n display: none !important;\n}\n\n/* ── viewport-level command menu ─────────────────────────────────────────\n The phone shell's nav rail creates a sibling stacking context and its\n center column can clip header descendants. The React menu therefore mounts\n in document.body; these styles deliberately detach it from that header\n context. Pixel coordinates (including the 56px rail exclusion) are set by\n positionMenu() in header.tsx. */\n.qc-menu-portal {\n position: fixed;\n right: auto;\n z-index: 1100;\n box-sizing: border-box;\n max-height: min(420px, calc(100dvh - 72px));\n}\n\n@media (max-width: 767px) {\n /* A 44px target lets a fingertip reliably activate both the stop and close\n buttons without changing the desktop header density. */\n body[data-dsh-mobile] .qc-popup-icon,\n [data-mobile-nav=\"frame\"] .qc-popup-icon {\n min-width: 44px;\n min-height: 44px;\n justify-content: center;\n }\n}";
1157
1157
  const tagId = "@fonlan/dsh-quick-commands/quick-commands.css#b65a4ce4";
1158
1158
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
package/lib/index.js CHANGED
@@ -688,6 +688,28 @@ const WorkspaceCommandsSchema = z.object({
688
688
  workspaceId: z.string().min(1),
689
689
  commands: z.array(CommandSchema).max(200)
690
690
  });
691
+ /**
692
+ * Entry config schema, and the schema dsh derives the plugin's settings page
693
+ * from. The `z<...>` annotation names the RESOLVED (live-handle) shape on
694
+ * purpose:
695
+ *
696
+ * - schemastery >= 3.18.3 carries a third `Mode` type parameter, so a
697
+ * `.volatile()` field's OUTPUT type is `Volatile<T>` (a live `{ get() }`
698
+ * cell), never `T` — the old `z<QuickCommandsSettings>` annotation stopped
699
+ * compiling;
700
+ * - dropping the annotation and letting the type be inferred does not build
701
+ * either: declaration emit then cannot portably name the inferred type
702
+ * (TS2742), because `z.array(...)` leaks `@deepseek-ai/cosmokit`'s `Dict`
703
+ * into the schema type while the program holds two schemastery copies
704
+ * (this plugin's 3.18.3, and the one `@deepseek-ai/dsh-host-webserver`
705
+ * resolves — see pnpm-workspace.yaml's minimumReleaseAgeExclude, which pins
706
+ * the former), and cosmokit itself is not nameable from this package.
707
+ *
708
+ * The annotation is type-only. At runtime all three fields are still declared
709
+ * `volatile()` — that is what makes them writable by the settings plane (its
710
+ * `write()` refuses an entry whose form has no volatile field) and what keeps
711
+ * the auto-generated page working.
712
+ */
691
713
  const QuickCommandsSettingsSchema = z.object({
692
714
  workspaces: z.array(WorkspaceCommandsSchema).max(500).volatile(),
693
715
  popupAnchor: z.union([z.const("corner"), z.const("button")]).default("corner").volatile(),
@@ -697,25 +719,30 @@ const QuickCommandsSettingsSchema = z.object({
697
719
  }).volatile()
698
720
  });
699
721
  /**
700
- * Read one config field. A volatile field arrives as a handle (dsh >= 0.1.7)
701
- * and is read through `.get()` on EVERY access, so edits made through the
702
- * settings plane are visible immediately; hosts that hand the plain value keep
703
- * working unchanged.
722
+ * Read a config node that may be a plain value or a live (`.volatile()`) node.
723
+ *
724
+ * A plugin whose Config declares volatile fields receives a reactive config
725
+ * whose fields expose `get()`, while hand-written config objects and tests pass
726
+ * plain values. Both shapes are supported here so the same code path serves the
727
+ * settings page, `cordis.patch.yml`, and the unit tests.
704
728
  */
705
- function readField(field, fallback) {
706
- if (field === void 0 || field === null) return fallback;
707
- const handle = field;
708
- const value = typeof handle.get === "function" ? handle.get() : field;
709
- return value === void 0 || value === null ? fallback : value;
729
+ function liveValue(node, fallback) {
730
+ if (node === void 0 || node === null) return fallback;
731
+ const getter = node.get;
732
+ if (typeof getter === "function") {
733
+ const value = getter.call(node);
734
+ return value === void 0 ? fallback : value;
735
+ }
736
+ return node;
710
737
  }
711
738
  function registerSettings(ctx, config) {
712
739
  const readWorkspaces = () => {
713
- const value = readField(config?.workspaces, []);
740
+ const value = liveValue(config?.workspaces, []);
714
741
  return Array.isArray(value) ? value : [];
715
742
  };
716
- const readAnchor = () => readField(config?.popupAnchor, "corner") === "button" ? "button" : "corner";
743
+ const readAnchor = () => liveValue(config?.popupAnchor, "corner") === "button" ? "button" : "corner";
717
744
  const readPopupSize = () => {
718
- const value = readField(config?.popupSize, void 0);
745
+ const value = liveValue(config?.popupSize, void 0);
719
746
  if (typeof value !== "object" || value === null) return void 0;
720
747
  const { width, height } = value;
721
748
  if (typeof width !== "number" || typeof height !== "number") return void 0;
@@ -13,9 +13,9 @@
13
13
  * absence of dsh-ssh degrades cleanly to local-only behaviour.
14
14
  */
15
15
  import type { Context } from '@deepseek-ai/cordis';
16
- import type { QuickCommandsSettings } from './shared/contract.js';
16
+ import { type QuickCommandsConfig } from './server/settings.js';
17
17
  export declare const name = "@fonlan/dsh-quick-commands";
18
18
  export declare const inject: string[];
19
- export declare const Config: import("@deepseek-ai/schemastery").default<QuickCommandsSettings>;
19
+ export declare const Config: import("@deepseek-ai/schemastery").default<import("./shared/contract.js").QuickCommandsSettings, import("./server/settings.js").QuickCommandsLiveSettings>;
20
20
  /** Configuration rides the entry config; edits persist through the settings service. */
21
- export declare function apply(ctx: Context, config: QuickCommandsSettings): void;
21
+ export declare function apply(ctx: Context, config: QuickCommandsConfig): void;
@@ -10,7 +10,7 @@
10
10
  * field therefore yields the handle object, which silently destroys the whole
11
11
  * document: `Array.isArray(handle)` is false, JSON renders `{}`, and a
12
12
  * `.default('corner')` union arrives as the handle rather than a literal.
13
- * Every read goes through {@link readField} instead, which unwraps handles and
13
+ * Every read goes through {@link liveValue} instead, which unwraps handles and
14
14
  * still accepts plain values (hosts that resolve the config without the
15
15
  * volatile wrapper).
16
16
  */
@@ -18,7 +18,49 @@ import type { Context } from '@deepseek-ai/cordis';
18
18
  import z from '@deepseek-ai/schemastery';
19
19
  import type { QuickCommandsSettings, QuickCommandEntry, QuickPopupSize } from '../shared/contract.js';
20
20
  export declare const QUICK_COMMANDS_NS = "quick-commands";
21
- export declare const QuickCommandsSettingsSchema: z<QuickCommandsSettings>;
21
+ /** Live handle schemastery wraps around one `volatile()` config field. */
22
+ interface VolatileField {
23
+ get(): unknown;
24
+ }
25
+ /**
26
+ * The entry config in its RESOLVED form: schemastery hands every `.volatile()`
27
+ * field to the loader (and so to `apply()`) as a live handle instead of the
28
+ * plain value. This is the schema's real output type.
29
+ */
30
+ export interface QuickCommandsLiveSettings {
31
+ workspaces: VolatileField;
32
+ popupAnchor: VolatileField;
33
+ popupSize: VolatileField;
34
+ }
35
+ /**
36
+ * The entry config as `apply()` receives it: the resolved (live) document, or
37
+ * the plain document in hand-built contexts and hosts that resolve the config
38
+ * without the volatile wrapper. Both load through the same `apply`.
39
+ */
40
+ export type QuickCommandsConfig = QuickCommandsSettings | QuickCommandsLiveSettings;
41
+ /**
42
+ * Entry config schema, and the schema dsh derives the plugin's settings page
43
+ * from. The `z<...>` annotation names the RESOLVED (live-handle) shape on
44
+ * purpose:
45
+ *
46
+ * - schemastery >= 3.18.3 carries a third `Mode` type parameter, so a
47
+ * `.volatile()` field's OUTPUT type is `Volatile<T>` (a live `{ get() }`
48
+ * cell), never `T` — the old `z<QuickCommandsSettings>` annotation stopped
49
+ * compiling;
50
+ * - dropping the annotation and letting the type be inferred does not build
51
+ * either: declaration emit then cannot portably name the inferred type
52
+ * (TS2742), because `z.array(...)` leaks `@deepseek-ai/cosmokit`'s `Dict`
53
+ * into the schema type while the program holds two schemastery copies
54
+ * (this plugin's 3.18.3, and the one `@deepseek-ai/dsh-host-webserver`
55
+ * resolves — see pnpm-workspace.yaml's minimumReleaseAgeExclude, which pins
56
+ * the former), and cosmokit itself is not nameable from this package.
57
+ *
58
+ * The annotation is type-only. At runtime all three fields are still declared
59
+ * `volatile()` — that is what makes them writable by the settings plane (its
60
+ * `write()` refuses an entry whose form has no volatile field) and what keeps
61
+ * the auto-generated page working.
62
+ */
63
+ export declare const QuickCommandsSettingsSchema: z<QuickCommandsSettings, QuickCommandsLiveSettings>;
22
64
  /** Settings service face for the quick-commands engine. */
23
65
  export interface QuickCommandsSettingsFace {
24
66
  get(): QuickCommandsSettings;
@@ -31,4 +73,5 @@ export interface QuickCommandsSettingsFace {
31
73
  /** Persist the output popup size (drag-resize). */
32
74
  setPopupSize(size: QuickPopupSize): Promise<void>;
33
75
  }
34
- export declare function registerSettings(ctx: Context, config: QuickCommandsSettings): QuickCommandsSettingsFace;
76
+ export declare function registerSettings(ctx: Context, config: QuickCommandsConfig): QuickCommandsSettingsFace;
77
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonlan/dsh-quick-commands",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "DSH web plugin: per-workspace quick commands. Configure named shell commands per DSH workspace, run one from the session header with live stdout/stderr in a floating popup.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,18 +44,6 @@
44
44
  "publishConfig": {
45
45
  "access": "public"
46
46
  },
47
- "scripts": {
48
- "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
49
- "build:client": "tsdown",
50
- "prepare": "npm run build",
51
- "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
52
- "test": "node --experimental-transform-types --test test/remote.test.ts test/settings-config.test.ts test/host-apply.test.ts test/shell-path.test.ts && node test/header-icons.test.mjs",
53
- "bundle": "tsdown",
54
- "watch": "tsdown --watch",
55
- "release:patch": "npm version patch && git push && git push --tags",
56
- "release:minor": "npm version minor && git push && git push --tags",
57
- "release:major": "npm version major && git push && git push --tags"
58
- },
59
47
  "dsh": {
60
48
  "bundle": {
61
49
  "patch": "./cordis.patch.yml"
@@ -75,33 +63,43 @@
75
63
  },
76
64
  "peerDependencies": {
77
65
  "@deepseek-ai/cordis": "^4.0.1",
78
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
79
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
80
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
81
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
82
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
83
- "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
84
- "@deepseek-ai/dsh-workspace": "^0.1.2-alpha.2",
66
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
67
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
68
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
69
+ "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
70
+ "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
71
+ "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
72
+ "@deepseek-ai/dsh-workspace": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
85
73
  "@deepseek-ai/schemastery": "^3.18.2",
86
74
  "react": "^18.2.0",
87
75
  "react-dom": "^18.2.0"
88
76
  },
89
77
  "devDependencies": {
90
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
91
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2",
92
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
93
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.2-alpha.2",
94
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
95
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
96
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
97
- "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
98
- "@deepseek-ai/dsh-workspace": "^0.1.2-alpha.2",
99
- "@deepseek-ai/schemastery": "^3.18.3",
78
+ "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.1",
79
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.1",
80
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.1",
81
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.1-alpha.1",
82
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.1",
83
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.1",
84
+ "@deepseek-ai/dsh-settings": "0.2.1-alpha.1",
85
+ "@deepseek-ai/dsh-subprocess": "0.2.1-alpha.1",
86
+ "@deepseek-ai/dsh-workspace": "0.2.1-alpha.1",
87
+ "@deepseek-ai/schemastery": "3.18.5-alpha.1",
100
88
  "@types/node": "^22.0.0",
101
89
  "@types/react": "~18.3.1",
102
90
  "@types/react-dom": "~18.3.0",
103
91
  "tsdown": "^0.22.14",
104
92
  "typescript": "^5.9.0"
105
93
  },
106
- "packageManager": "pnpm@11.21.0"
107
- }
94
+ "scripts": {
95
+ "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
96
+ "build:client": "tsdown",
97
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
98
+ "test": "node --experimental-transform-types --test test/remote.test.ts test/settings-config.test.ts test/host-apply.test.ts test/shell-path.test.ts && node test/header-icons.test.mjs",
99
+ "bundle": "tsdown",
100
+ "watch": "tsdown --watch",
101
+ "release:patch": "npm version patch && git push && git push --tags",
102
+ "release:minor": "npm version minor && git push && git push --tags",
103
+ "release:major": "npm version major && git push && git push --tags"
104
+ }
105
+ }
package/src/index.ts CHANGED
@@ -15,9 +15,8 @@
15
15
  import type { Context } from '@deepseek-ai/cordis'
16
16
  import { QuickCommandRunner, type RemoteBackend } from './server/runner.js'
17
17
  import { readHostConfig } from './server/remote.js'
18
- import { QuickCommandsSettingsSchema, registerSettings } from './server/settings.js'
18
+ import { QuickCommandsSettingsSchema, registerSettings, type QuickCommandsConfig } from './server/settings.js'
19
19
  import { registerApiRoutes } from './server/rpc.js'
20
- import type { QuickCommandsSettings } from './shared/contract.js'
21
20
 
22
21
  export const name = '@fonlan/dsh-quick-commands'
23
22
 
@@ -30,7 +29,7 @@ export const inject = ['settings']
30
29
  export const Config = QuickCommandsSettingsSchema
31
30
 
32
31
  /** Configuration rides the entry config; edits persist through the settings service. */
33
- export function apply(ctx: Context, config: QuickCommandsSettings): void {
32
+ export function apply(ctx: Context, config: QuickCommandsConfig): void {
34
33
  const settings = registerSettings(ctx, config)
35
34
  // dsh >= 0.1.7 activates entries asynchronously: a fiber is only readable as a
36
35
  // service once its own init settled, and the webserver's init waits for its
@@ -10,7 +10,7 @@
10
10
  * field therefore yields the handle object, which silently destroys the whole
11
11
  * document: `Array.isArray(handle)` is false, JSON renders `{}`, and a
12
12
  * `.default('corner')` union arrives as the handle rather than a literal.
13
- * Every read goes through {@link readField} instead, which unwraps handles and
13
+ * Every read goes through {@link liveValue} instead, which unwraps handles and
14
14
  * still accepts plain values (hosts that resolve the config without the
15
15
  * volatile wrapper).
16
16
  */
@@ -36,7 +36,52 @@ const WorkspaceCommandsSchema = z.object({
36
36
  commands: z.array(CommandSchema).max(200),
37
37
  })
38
38
 
39
- export const QuickCommandsSettingsSchema: z<QuickCommandsSettings> = z.object({
39
+ /** Live handle schemastery wraps around one `volatile()` config field. */
40
+ interface VolatileField {
41
+ get(): unknown
42
+ }
43
+
44
+ /**
45
+ * The entry config in its RESOLVED form: schemastery hands every `.volatile()`
46
+ * field to the loader (and so to `apply()`) as a live handle instead of the
47
+ * plain value. This is the schema's real output type.
48
+ */
49
+ export interface QuickCommandsLiveSettings {
50
+ workspaces: VolatileField
51
+ popupAnchor: VolatileField
52
+ popupSize: VolatileField
53
+ }
54
+
55
+ /**
56
+ * The entry config as `apply()` receives it: the resolved (live) document, or
57
+ * the plain document in hand-built contexts and hosts that resolve the config
58
+ * without the volatile wrapper. Both load through the same `apply`.
59
+ */
60
+ export type QuickCommandsConfig = QuickCommandsSettings | QuickCommandsLiveSettings
61
+
62
+ /**
63
+ * Entry config schema, and the schema dsh derives the plugin's settings page
64
+ * from. The `z<...>` annotation names the RESOLVED (live-handle) shape on
65
+ * purpose:
66
+ *
67
+ * - schemastery >= 3.18.3 carries a third `Mode` type parameter, so a
68
+ * `.volatile()` field's OUTPUT type is `Volatile<T>` (a live `{ get() }`
69
+ * cell), never `T` — the old `z<QuickCommandsSettings>` annotation stopped
70
+ * compiling;
71
+ * - dropping the annotation and letting the type be inferred does not build
72
+ * either: declaration emit then cannot portably name the inferred type
73
+ * (TS2742), because `z.array(...)` leaks `@deepseek-ai/cosmokit`'s `Dict`
74
+ * into the schema type while the program holds two schemastery copies
75
+ * (this plugin's 3.18.3, and the one `@deepseek-ai/dsh-host-webserver`
76
+ * resolves — see pnpm-workspace.yaml's minimumReleaseAgeExclude, which pins
77
+ * the former), and cosmokit itself is not nameable from this package.
78
+ *
79
+ * The annotation is type-only. At runtime all three fields are still declared
80
+ * `volatile()` — that is what makes them writable by the settings plane (its
81
+ * `write()` refuses an entry whose form has no volatile field) and what keeps
82
+ * the auto-generated page working.
83
+ */
84
+ export const QuickCommandsSettingsSchema: z<QuickCommandsSettings, QuickCommandsLiveSettings> = z.object({
40
85
  // Values are addressed by index (array edits), so the whole array is the
41
86
  // volatile handle the settings plane swaps in place.
42
87
  workspaces: z.array(WorkspaceCommandsSchema).max(500).volatile(),
@@ -51,22 +96,22 @@ export const QuickCommandsSettingsSchema: z<QuickCommandsSettings> = z.object({
51
96
  }).volatile(),
52
97
  })
53
98
 
54
- /** Live handle schemastery wraps around one `volatile()` config field. */
55
- interface VolatileField<T> {
56
- get(): T
57
- }
58
-
59
99
  /**
60
- * Read one config field. A volatile field arrives as a handle (dsh >= 0.1.7)
61
- * and is read through `.get()` on EVERY access, so edits made through the
62
- * settings plane are visible immediately; hosts that hand the plain value keep
63
- * working unchanged.
100
+ * Read a config node that may be a plain value or a live (`.volatile()`) node.
101
+ *
102
+ * A plugin whose Config declares volatile fields receives a reactive config
103
+ * whose fields expose `get()`, while hand-written config objects and tests pass
104
+ * plain values. Both shapes are supported here so the same code path serves the
105
+ * settings page, `cordis.patch.yml`, and the unit tests.
64
106
  */
65
- function readField<T>(field: unknown, fallback: T): T {
66
- if (field === undefined || field === null) return fallback
67
- const handle = field as Partial<VolatileField<T>>
68
- const value = typeof handle.get === 'function' ? handle.get() : (field as T)
69
- return value === undefined || value === null ? fallback : value
107
+ function liveValue<T>(node: unknown, fallback: T): T {
108
+ if (node === undefined || node === null) return fallback
109
+ const getter = (node as { get?: unknown }).get
110
+ if (typeof getter === 'function') {
111
+ const value = (getter as () => unknown).call(node)
112
+ return value === undefined ? fallback : (value as T)
113
+ }
114
+ return node as T
70
115
  }
71
116
 
72
117
  /** Structural settings-service subset used for persistence. */
@@ -87,15 +132,15 @@ export interface QuickCommandsSettingsFace {
87
132
  setPopupSize(size: QuickPopupSize): Promise<void>
88
133
  }
89
134
 
90
- export function registerSettings(ctx: Context, config: QuickCommandsSettings): QuickCommandsSettingsFace {
135
+ export function registerSettings(ctx: Context, config: QuickCommandsConfig): QuickCommandsSettingsFace {
91
136
  const readWorkspaces = (): WorkspaceCommands[] => {
92
- const value = readField<unknown>(config?.workspaces, [])
137
+ const value = liveValue<unknown>(config?.workspaces, [])
93
138
  return Array.isArray(value) ? value as WorkspaceCommands[] : []
94
139
  }
95
140
  const readAnchor = (): QuickCommandsSettings['popupAnchor'] =>
96
- (readField<unknown>(config?.popupAnchor, 'corner') === 'button' ? 'button' : 'corner')
141
+ (liveValue<unknown>(config?.popupAnchor, 'corner') === 'button' ? 'button' : 'corner')
97
142
  const readPopupSize = (): QuickPopupSize | undefined => {
98
- const value = readField<unknown>(config?.popupSize, undefined)
143
+ const value = liveValue<unknown>(config?.popupSize, undefined)
99
144
  if (typeof value !== 'object' || value === null) return undefined
100
145
  const { width, height } = value as { width?: unknown; height?: unknown }
101
146
  // `{}` (never resized) and any partially written size read as absent.