@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 +2 -2
- package/lib/index.js +39 -12
- package/lib/types/index.d.ts +3 -3
- package/lib/types/server/settings.d.ts +46 -3
- package/package.json +30 -32
- package/src/index.ts +2 -3
- package/src/server/settings.ts +65 -20
package/lib/client.js
CHANGED
|
@@ -825,7 +825,7 @@ window.__ModuleLoader__.load({
|
|
|
825
825
|
});
|
|
826
826
|
}
|
|
827
827
|
//#endregion
|
|
828
|
-
//#region \0dsh-css:/
|
|
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:/
|
|
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
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
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
|
|
706
|
-
if (
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
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 =
|
|
740
|
+
const value = liveValue(config?.workspaces, []);
|
|
714
741
|
return Array.isArray(value) ? value : [];
|
|
715
742
|
};
|
|
716
|
-
const readAnchor = () =>
|
|
743
|
+
const readAnchor = () => liveValue(config?.popupAnchor, "corner") === "button" ? "button" : "corner";
|
|
717
744
|
const readPopupSize = () => {
|
|
718
|
-
const value =
|
|
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;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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": "
|
|
91
|
-
"@deepseek-ai/dsh-client-ui-conversation": "
|
|
92
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
93
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "
|
|
94
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
95
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
96
|
-
"@deepseek-ai/dsh-settings": "
|
|
97
|
-
"@deepseek-ai/dsh-subprocess": "
|
|
98
|
-
"@deepseek-ai/dsh-workspace": "
|
|
99
|
-
"@deepseek-ai/schemastery": "
|
|
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
|
-
"
|
|
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:
|
|
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
|
package/src/server/settings.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
66
|
-
if (
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
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:
|
|
135
|
+
export function registerSettings(ctx: Context, config: QuickCommandsConfig): QuickCommandsSettingsFace {
|
|
91
136
|
const readWorkspaces = (): WorkspaceCommands[] => {
|
|
92
|
-
const value =
|
|
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
|
-
(
|
|
141
|
+
(liveValue<unknown>(config?.popupAnchor, 'corner') === 'button' ? 'button' : 'corner')
|
|
97
142
|
const readPopupSize = (): QuickPopupSize | undefined => {
|
|
98
|
-
const value =
|
|
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.
|