@qping/plugin-bus 0.2.0 → 0.4.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/dist/node.mjs CHANGED
@@ -167,6 +167,7 @@ var Routes = {
167
167
  Initialize: "host.event.initialize",
168
168
  Search: "host.event.search",
169
169
  Key: "host.event.key",
170
+ DetailAction: "host.event.detailAction",
170
171
  LanguageChanged: "host.event.languageChanged",
171
172
  ThemeChanged: "host.event.themeChanged",
172
173
  InputActionCaptured: "host.event.inputActionCaptured"
@@ -339,7 +340,10 @@ var HandlerRouter = class {
339
340
  }
340
341
  const deadlineMs = deadlineFromTimeoutMs(env.timeoutMs);
341
342
  try {
342
- const result = await requestScope.run({ deadlineMs }, () => handler(env.payload));
343
+ const result = await requestScope.run(
344
+ { deadlineMs },
345
+ () => handler(env.payload, { sessionId: env.sessionId })
346
+ );
343
347
  this.send(this.responseFor(env, result ?? {}));
344
348
  } catch (err) {
345
349
  const message = err instanceof Error ? err.message : String(err);
@@ -527,13 +531,120 @@ async function completeHandshake(transport, token, timeoutMs = 1e4) {
527
531
  });
528
532
  }
529
533
 
534
+ // src/hostEnv.ts
535
+ function asTheme(value) {
536
+ return value === "light" ? "light" : "dark";
537
+ }
538
+ function asHostEnv(payload) {
539
+ return {
540
+ locale: typeof payload?.locale === "string" ? payload.locale : "en-US",
541
+ fallbackLocale: typeof payload?.fallbackLocale === "string" ? payload.fallbackLocale : "en-US",
542
+ theme: asTheme(payload?.theme)
543
+ };
544
+ }
545
+
546
+ // src/actions.ts
547
+ var Key = {
548
+ Enter: "Enter",
549
+ Tab: "Tab",
550
+ Space: "Space",
551
+ Delete: "Delete",
552
+ Backspace: "Backspace",
553
+ Escape: "Escape",
554
+ Left: "Left",
555
+ Right: "Right",
556
+ Up: "Up",
557
+ Down: "Down",
558
+ A: "A",
559
+ B: "B",
560
+ C: "C",
561
+ D: "D",
562
+ E: "E",
563
+ F: "F",
564
+ G: "G",
565
+ H: "H",
566
+ I: "I",
567
+ J: "J",
568
+ K: "K",
569
+ L: "L",
570
+ M: "M",
571
+ N: "N",
572
+ O: "O",
573
+ P: "P",
574
+ Q: "Q",
575
+ R: "R",
576
+ S: "S",
577
+ T: "T",
578
+ U: "U",
579
+ V: "V",
580
+ W: "W",
581
+ X: "X",
582
+ Y: "Y",
583
+ Z: "Z",
584
+ D0: "D0",
585
+ D1: "D1",
586
+ D2: "D2",
587
+ D3: "D3",
588
+ D4: "D4",
589
+ D5: "D5",
590
+ D6: "D6",
591
+ D7: "D7",
592
+ D8: "D8",
593
+ D9: "D9",
594
+ F1: "F1",
595
+ F2: "F2",
596
+ F3: "F3",
597
+ F4: "F4",
598
+ F5: "F5",
599
+ F6: "F6",
600
+ F7: "F7",
601
+ F8: "F8",
602
+ F9: "F9",
603
+ F10: "F10",
604
+ F11: "F11",
605
+ F12: "F12"
606
+ };
607
+ var Modifiers = {
608
+ None: 0,
609
+ Control: 1,
610
+ Alt: 2,
611
+ ControlAlt: 3,
612
+ Shift: 4,
613
+ ControlShift: 5,
614
+ AltShift: 6,
615
+ ControlAltShift: 7
616
+ };
617
+ var HostAction = {
618
+ Copy: "copy",
619
+ CopyAndPaste: "copyAndPaste",
620
+ AddClipboardHistory: "addClipboardHistory",
621
+ Execute: "execute",
622
+ OpenInExplorer: "openInExplorer",
623
+ OpenInBrowser: "openInBrowser",
624
+ OpenPlugin: "openPlugin",
625
+ Run: "run",
626
+ Kill: "kill"
627
+ };
628
+ function toActionManifest(definition) {
629
+ const entry = {
630
+ id: definition.id,
631
+ title: definition.title
632
+ };
633
+ if (definition.description) entry.description = definition.description;
634
+ if (definition.hotkey) entry.hotkey = definition.hotkey;
635
+ return entry;
636
+ }
637
+
530
638
  // src/node.ts
639
+ var ItemCacheLimit = 1e3;
640
+ var SessionCacheLimit = 8;
531
641
  var Plugin = class {
532
642
  #handlers = /* @__PURE__ */ new Map();
643
+ #actions = /* @__PURE__ */ new Map();
533
644
  #searchHandler = null;
534
- #actionHandler = null;
535
645
  #initializeHandler = null;
536
646
  #runtime = null;
647
+ #itemsBySession = /* @__PURE__ */ new Map();
537
648
  initialize(handler) {
538
649
  this.#initializeHandler = handler;
539
650
  return this;
@@ -542,8 +653,27 @@ var Plugin = class {
542
653
  this.#searchHandler = handler;
543
654
  return this;
544
655
  }
545
- action(handler) {
546
- this.#actionHandler = handler;
656
+ /**
657
+ * Registers every action this plugin offers. The list is sent to the host in the initialize
658
+ * response, so the host knows the ids, labels and hotkeys before any search runs; search items
659
+ * and the detail page then reference them by id only.
660
+ */
661
+ actions(definitions) {
662
+ if (!Array.isArray(definitions)) {
663
+ throw new Error("plugin.actions requires an array of action definitions.");
664
+ }
665
+ for (const definition of definitions) {
666
+ if (!definition?.id) {
667
+ throw new Error("plugin.actions requires every action to have an id.");
668
+ }
669
+ if (this.#actions.has(definition.id)) {
670
+ throw new Error(`plugin.actions has a duplicate action id: ${definition.id}`);
671
+ }
672
+ if (typeof definition.execute !== "function") {
673
+ throw new Error(`plugin.actions requires an execute function for action: ${definition.id}`);
674
+ }
675
+ this.#actions.set(definition.id, definition);
676
+ }
547
677
  return this;
548
678
  }
549
679
  handle(action, handler) {
@@ -592,14 +722,19 @@ var Plugin = class {
592
722
  */
593
723
  buildRoutes() {
594
724
  const routes = {};
595
- if (this.#initializeHandler) {
596
- routes[Routes.PluginCall.Initialize] = (p) => this.#initializeHandler(asInitializeParams(p));
597
- }
725
+ routes[Routes.PluginCall.Initialize] = async (p) => {
726
+ const result = this.#initializeHandler ? await this.#initializeHandler(asInitializeParams(p)) : {};
727
+ const body = result && typeof result === "object" ? { ...result } : {};
728
+ return { ...body, actions: [...this.#actions.values()].map(toActionManifest) };
729
+ };
598
730
  if (this.#searchHandler) {
599
- routes[Routes.PluginCall.Search] = (p) => this.#searchHandler(asSearchParams(p));
731
+ routes[Routes.PluginCall.Search] = async (p, request) => {
732
+ const result = await this.#searchHandler(asSearchParams(p));
733
+ return { items: this.#trackItems(request?.sessionId ?? "default", result?.items ?? []) };
734
+ };
600
735
  }
601
- if (this.#actionHandler) {
602
- routes[Routes.PluginCall.InvokeAction] = (p) => this.#actionHandler(asActionParams(p));
736
+ if (this.#actions.size > 0) {
737
+ routes[Routes.PluginCall.InvokeAction] = (p, request) => this.#invokeAction(request?.sessionId ?? "default", p);
603
738
  }
604
739
  for (const [action, handler] of this.#handlers) {
605
740
  const route = pluginCallRoute(action);
@@ -615,13 +750,69 @@ var Plugin = class {
615
750
  async stop() {
616
751
  if (this.#runtime) await this.#runtime.close();
617
752
  }
753
+ /** Remembers the full items and returns the trimmed rows the host actually renders. */
754
+ #trackItems(sessionId, items) {
755
+ const sessionItems = this.#sessionItems(sessionId);
756
+ const wire = [];
757
+ for (const item of items) {
758
+ if (!item || typeof item !== "object") continue;
759
+ const id = typeof item.id === "string" ? item.id : "";
760
+ if (id) {
761
+ sessionItems.delete(id);
762
+ sessionItems.set(id, item);
763
+ }
764
+ wire.push(toWireItem(item));
765
+ }
766
+ while (sessionItems.size > ItemCacheLimit) {
767
+ const oldest = sessionItems.keys().next();
768
+ if (oldest.done) break;
769
+ sessionItems.delete(oldest.value);
770
+ }
771
+ return wire;
772
+ }
773
+ async #invokeAction(sessionId, payload) {
774
+ const env = asHostEnv(payload);
775
+ const actionId = typeof payload?.actionId === "string" ? payload.actionId : "";
776
+ const itemId = typeof payload?.itemId === "string" ? payload.itemId : "";
777
+ const query = typeof payload?.query === "string" ? payload.query : "";
778
+ const definition = this.#actions.get(actionId);
779
+ if (!definition) {
780
+ throw new Error(`unknown action: ${actionId}`);
781
+ }
782
+ const outcome = await definition.execute({
783
+ ...env,
784
+ actionId,
785
+ itemId,
786
+ query,
787
+ item: this.#itemsBySession.get(sessionId)?.get(itemId)
788
+ });
789
+ return outcome ?? {};
790
+ }
791
+ #sessionItems(sessionId) {
792
+ const key = sessionId || "default";
793
+ let items = this.#itemsBySession.get(key);
794
+ if (!items) {
795
+ items = /* @__PURE__ */ new Map();
796
+ this.#itemsBySession.set(key, items);
797
+ while (this.#itemsBySession.size > SessionCacheLimit) {
798
+ const oldest = this.#itemsBySession.keys().next();
799
+ if (oldest.done) break;
800
+ this.#itemsBySession.delete(oldest.value);
801
+ }
802
+ }
803
+ return items;
804
+ }
618
805
  };
619
- function asHostEnv(p) {
620
- return {
621
- locale: typeof p?.locale === "string" ? p.locale : "en-US",
622
- fallbackLocale: typeof p?.fallbackLocale === "string" ? p.fallbackLocale : "en-US",
623
- theme: asTheme(p?.theme)
806
+ function toWireItem(item) {
807
+ const wire = {
808
+ id: item.id,
809
+ title: item.title
624
810
  };
811
+ if (typeof item.subtitle === "string") wire.subtitle = item.subtitle;
812
+ if (typeof item.priority === "number") wire.priority = item.priority;
813
+ if (item.icon) wire.icon = item.icon;
814
+ if (Array.isArray(item.actions)) wire.actions = item.actions.filter((id) => typeof id === "string");
815
+ return wire;
625
816
  }
626
817
  function asInitializeParams(p) {
627
818
  const messages = p?.messages;
@@ -637,17 +828,6 @@ function asSearchParams(p) {
637
828
  mode: p?.mode === "plugin" ? "plugin" : "global"
638
829
  };
639
830
  }
640
- function asActionParams(p) {
641
- return {
642
- ...asHostEnv(p),
643
- itemId: typeof p?.itemId === "string" ? p.itemId : "",
644
- actionId: typeof p?.actionId === "string" ? p.actionId : "",
645
- query: typeof p?.query === "string" ? p.query : ""
646
- };
647
- }
648
- function asTheme(value) {
649
- return value === "light" ? "light" : "dark";
650
- }
651
831
  function isStringRecord(value) {
652
832
  return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
653
833
  }
@@ -663,6 +843,9 @@ function createPlugin() {
663
843
  return new Plugin();
664
844
  }
665
845
  export {
846
+ HostAction,
847
+ Key,
848
+ Modifiers,
666
849
  Plugin,
667
850
  createPlugin
668
851
  };
@@ -62,6 +62,7 @@ export declare const Routes: {
62
62
  readonly Initialize: "host.event.initialize";
63
63
  readonly Search: "host.event.search";
64
64
  readonly Key: "host.event.key";
65
+ readonly DetailAction: "host.event.detailAction";
65
66
  readonly LanguageChanged: "host.event.languageChanged";
66
67
  readonly ThemeChanged: "host.event.themeChanged";
67
68
  readonly InputActionCaptured: "host.event.inputActionCaptured";
package/dist/protocol.mjs CHANGED
@@ -49,6 +49,7 @@ var Routes = {
49
49
  Initialize: "host.event.initialize",
50
50
  Search: "host.event.search",
51
51
  Key: "host.event.key",
52
+ DetailAction: "host.event.detailAction",
52
53
  LanguageChanged: "host.event.languageChanged",
53
54
  ThemeChanged: "host.event.themeChanged",
54
55
  InputActionCaptured: "host.event.inputActionCaptured"
package/dist/router.d.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  * and correlate their responses. Mirrors the C# MessageBus routing rules on the Node side.
5
5
  */
6
6
  import { type Envelope } from "./protocol.ts";
7
- type Handler = (payload: unknown) => Promise<unknown> | unknown;
7
+ type Handler = (payload: unknown, context: {
8
+ sessionId: string;
9
+ }) => Promise<unknown> | unknown;
8
10
  type Sender = (env: Envelope) => void;
9
11
  export declare const DefaultHostCallTimeoutMs = 30000;
10
12
  /** Remaining ms of the inbound plugin.call timeout, if currently inside a request. */
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns whether every character in `pattern` occurs in `target` in the same order.
3
+ * Matching is case-insensitive and characters do not need to be adjacent.
4
+ *
5
+ * @example isSubsequence("gthb", "GitHub") // true
6
+ */
7
+ export declare function isSubsequence(pattern: string, target: string): boolean;
@@ -0,0 +1,17 @@
1
+ // src/search.ts
2
+ function isSubsequence(pattern, target) {
3
+ if (!pattern) return true;
4
+ if (!target) return false;
5
+ const needle = pattern.toLowerCase();
6
+ const haystack = target.toLowerCase();
7
+ let patternIndex = 0;
8
+ for (let targetIndex = 0; targetIndex < haystack.length && patternIndex < needle.length; targetIndex += 1) {
9
+ if (haystack[targetIndex] === needle[patternIndex]) {
10
+ patternIndex += 1;
11
+ }
12
+ }
13
+ return patternIndex === needle.length;
14
+ }
15
+ export {
16
+ isSubsequence
17
+ };
@@ -8,8 +8,9 @@
8
8
  import { mytoolsI18n } from "./i18n.ts";
9
9
  import type { MyToolsThemePayload } from "./webTypes.ts";
10
10
  export { mytoolsI18n } from "./i18n.ts";
11
+ export { renderHotkeyKeycaps } from "./hotkeyKeycaps.ts";
11
12
  export { HostEvents } from "./webTypes.ts";
12
- export type { MyToolsHostInitializePayload, MyToolsHostKeyPayload, MyToolsHostSearchPayload, MyToolsInputActionCapturedPayload, MyToolsLanguageChangedPayload, MyToolsThemeChangedPayload, MyToolsThemePayload, } from "./webTypes.ts";
13
+ export type { MyToolsHostActionDefinition, MyToolsHostDetailActionPayload, MyToolsHostInitializePayload, MyToolsHostKeyPayload, MyToolsHostSearchPayload, MyToolsInputActionCapturedPayload, MyToolsLanguageChangedPayload, MyToolsThemeChangedPayload, MyToolsThemePayload, } from "./webTypes.ts";
13
14
  export interface WebBusClient {
14
15
  /** Sends plugin.call.<method>. Bare names are prefixed; full routes are left as-is. */
15
16
  call<T = unknown>(method: string, payload?: unknown, timeoutMs?: number): Promise<T>;
@@ -2370,6 +2370,7 @@ var Routes = {
2370
2370
  Initialize: "host.event.initialize",
2371
2371
  Search: "host.event.search",
2372
2372
  Key: "host.event.key",
2373
+ DetailAction: "host.event.detailAction",
2373
2374
  LanguageChanged: "host.event.languageChanged",
2374
2375
  ThemeChanged: "host.event.themeChanged",
2375
2376
  InputActionCaptured: "host.event.inputActionCaptured"
@@ -2379,6 +2380,29 @@ function pluginCallRoute(method) {
2379
2380
  return method.startsWith(Routes.Prefix.PluginCall) ? method : `${Routes.Prefix.PluginCall}${method}`;
2380
2381
  }
2381
2382
 
2383
+ // src/hotkeyKeycaps.ts
2384
+ function renderHotkeyKeycaps(element, hotkey) {
2385
+ const normalized = hotkey.trim();
2386
+ element.hidden = normalized.length === 0;
2387
+ element.setAttribute("aria-label", normalized);
2388
+ element.classList.add("hotkey-keycaps");
2389
+ const keycaps = normalized.length === 0 ? [] : normalized.split("+").map((token) => createKeycap(token.trim()));
2390
+ element.replaceChildren(...keycaps);
2391
+ }
2392
+ function createKeycap(token) {
2393
+ const keycap = document.createElement("span");
2394
+ keycap.className = "hotkey-keycap";
2395
+ keycap.setAttribute("aria-hidden", "true");
2396
+ if (token.toLowerCase() === "enter" || token.toLowerCase() === "return") {
2397
+ keycap.classList.add("hotkey-keycap-enter");
2398
+ keycap.textContent = "\u21B5";
2399
+ keycap.title = "Enter";
2400
+ } else {
2401
+ keycap.textContent = token;
2402
+ }
2403
+ return keycap;
2404
+ }
2405
+
2382
2406
  // src/webTypes.ts
2383
2407
  var HostEvents = Routes.HostEvent;
2384
2408
 
@@ -2595,5 +2619,6 @@ function handshake(timeoutMs) {
2595
2619
  export {
2596
2620
  HostEvents,
2597
2621
  createWebBusClient,
2598
- mytoolsI18n
2622
+ mytoolsI18n,
2623
+ renderHotkeyKeycaps
2599
2624
  };
@@ -1,3 +1,10 @@
1
+ export interface MyToolsHostActionDefinition {
2
+ id: string;
3
+ /** Host-localized action display name. */
4
+ name: string;
5
+ /** Host-validated display form, for example `Ctrl+H`; absent means click-only. */
6
+ hotkey?: string | null;
7
+ }
1
8
  export interface MyToolsHostInitializePayload {
2
9
  protocolVersion: string;
3
10
  pluginId: string;
@@ -10,6 +17,7 @@ export interface MyToolsHostInitializePayload {
10
17
  fallbackLocale: string;
11
18
  translationRevision: string;
12
19
  messages: Record<string, string>;
20
+ actions: MyToolsHostActionDefinition[];
13
21
  theme?: string;
14
22
  themeTokens?: Record<string, string>;
15
23
  }
@@ -29,6 +37,12 @@ export interface MyToolsHostSearchPayload {
29
37
  export interface MyToolsHostKeyPayload {
30
38
  key: string;
31
39
  }
40
+ /** Payload explicitly returned in an action outcome's `web.payload`. */
41
+ export interface MyToolsHostDetailActionPayload {
42
+ actionId?: string;
43
+ action?: string;
44
+ [key: string]: unknown;
45
+ }
32
46
  export interface MyToolsInputActionCapturedPayload {
33
47
  requestId: string;
34
48
  cancelled?: boolean;
@@ -44,6 +58,7 @@ export declare const HostEvents: {
44
58
  readonly Initialize: "host.event.initialize";
45
59
  readonly Search: "host.event.search";
46
60
  readonly Key: "host.event.key";
61
+ readonly DetailAction: "host.event.detailAction";
47
62
  readonly LanguageChanged: "host.event.languageChanged";
48
63
  readonly ThemeChanged: "host.event.themeChanged";
49
64
  readonly InputActionCaptured: "host.event.inputActionCaptured";
package/package.json CHANGED
@@ -1,50 +1,60 @@
1
- {
2
- "name": "@qping/plugin-bus",
3
- "version": "0.2.0",
4
- "description": "v3 message-bus runtime for MyTools plugins (named-pipe transport).",
5
- "type": "module",
6
- "files": [
7
- "dist",
8
- "src"
9
- ],
10
- "exports": {
11
- "./node": {
12
- "types": "./dist/node.d.ts",
13
- "import": "./dist/node.mjs",
14
- "default": "./dist/node.mjs"
15
- },
16
- "./protocol": {
17
- "types": "./dist/protocol.d.ts",
18
- "import": "./dist/protocol.mjs",
19
- "default": "./dist/protocol.mjs"
20
- },
21
- "./bootstrap": {
22
- "types": "./dist/bootstrap.d.ts",
23
- "import": "./dist/bootstrap.mjs",
24
- "default": "./dist/bootstrap.mjs"
25
- },
26
- "./web": {
27
- "types": "./dist/webClient.d.ts",
28
- "import": "./dist/webClient.mjs",
29
- "default": "./dist/webClient.mjs"
30
- },
31
- "./i18n": {
32
- "types": "./dist/i18n.d.ts",
33
- "import": "./dist/i18n.mjs",
34
- "default": "./dist/i18n.mjs"
35
- },
36
- "./package.json": "./package.json"
37
- },
38
- "dependencies": {
39
- "i18next": "^25.3.2"
40
- },
41
- "scripts": {
42
- "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
43
- "build": "npm run clean && node build-sdk.mjs",
44
- "check": "node build-sdk.mjs"
45
- },
46
- "devDependencies": {
47
- "esbuild": "^0.25.8",
48
- "typescript": "^7.0.2"
49
- }
50
- }
1
+ {
2
+ "name": "@qping/plugin-bus",
3
+ "version": "0.4.0",
4
+ "description": "v3 message-bus runtime for MyTools plugins (named-pipe transport).",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "src"
9
+ ],
10
+ "exports": {
11
+ "./node": {
12
+ "types": "./dist/node.d.ts",
13
+ "import": "./dist/node.mjs",
14
+ "default": "./dist/node.mjs"
15
+ },
16
+ "./protocol": {
17
+ "types": "./dist/protocol.d.ts",
18
+ "import": "./dist/protocol.mjs",
19
+ "default": "./dist/protocol.mjs"
20
+ },
21
+ "./bootstrap": {
22
+ "types": "./dist/bootstrap.d.ts",
23
+ "import": "./dist/bootstrap.mjs",
24
+ "default": "./dist/bootstrap.mjs"
25
+ },
26
+ "./web": {
27
+ "types": "./dist/webClient.d.ts",
28
+ "import": "./dist/webClient.mjs",
29
+ "default": "./dist/webClient.mjs"
30
+ },
31
+ "./i18n": {
32
+ "types": "./dist/i18n.d.ts",
33
+ "import": "./dist/i18n.mjs",
34
+ "default": "./dist/i18n.mjs"
35
+ },
36
+ "./search": {
37
+ "types": "./dist/search.d.ts",
38
+ "import": "./dist/search.mjs",
39
+ "default": "./dist/search.mjs"
40
+ },
41
+ "./dev": {
42
+ "types": "./dist/dev.d.ts",
43
+ "import": "./dist/dev.mjs",
44
+ "default": "./dist/dev.mjs"
45
+ },
46
+ "./package.json": "./package.json"
47
+ },
48
+ "dependencies": {
49
+ "i18next": "^25.3.2"
50
+ },
51
+ "scripts": {
52
+ "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
53
+ "build": "npm run clean && node build-sdk.mjs",
54
+ "check": "node build-sdk.mjs"
55
+ },
56
+ "devDependencies": {
57
+ "esbuild": "^0.25.8",
58
+ "typescript": "^7.0.2"
59
+ }
60
+ }