@harborclient/sdk 1.0.64 → 1.0.66

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.
@@ -13,9 +13,116 @@ import { setHostReact } from './reactHost.js';
13
13
  /** @type {Map<string, Set<(...args: unknown[]) => void | Promise<void>>>} */
14
14
  const commandHandlers = new Map();
15
15
 
16
+ /** @type {Map<string, import('../types').ImportHandler>} */
17
+ const importHandlersByRegistrationId = new Map();
18
+
19
+ /** Monotonic id generator for import handler registrations within one webview. */
20
+ let importRegistrationCounter = 0;
21
+
16
22
  /** Plugin id prefix for built-in HarborClient host commands executed in the renderer. */
17
23
  const HOST_COMMAND_OWNER = 'harborclient';
18
24
 
25
+ /** Guards repeated import invoke listener installation per webview load. */
26
+ let importInvokeListenerInstalled = false;
27
+
28
+ /**
29
+ * Normalizes a file extension to lowercase with a leading dot.
30
+ *
31
+ * @param {string} extension - Extension with or without a leading dot.
32
+ * @returns {string} Normalized extension such as `.json`, or an empty string when absent.
33
+ */
34
+ function normalizeImportExtension(extension) {
35
+ const trimmed = extension.trim().toLowerCase();
36
+ if (!trimmed) {
37
+ return '';
38
+ }
39
+ return trimmed.startsWith('.') ? trimmed : `.${trimmed}`;
40
+ }
41
+
42
+ /**
43
+ * Normalizes one extension or an array of extensions for handler registration.
44
+ *
45
+ * @param {string | string[]} extensions - Single extension or list of extensions.
46
+ * @returns {string[]} Deduplicated normalized extensions.
47
+ */
48
+ function normalizeImportExtensions(extensions) {
49
+ const values = Array.isArray(extensions) ? extensions : [extensions];
50
+ const normalized = values
51
+ .map((extension) => normalizeImportExtension(extension))
52
+ .filter((extension) => extension.length > 0);
53
+ return [...new Set(normalized)];
54
+ }
55
+
56
+ /**
57
+ * Subscribes to host-initiated import handler invocations for the agent webview.
58
+ *
59
+ * Must run once before plugin activation so File → Import can reach registered handlers.
60
+ */
61
+ export function installImportInvokeListener() {
62
+ if (importInvokeListenerInstalled) {
63
+ return;
64
+ }
65
+ importInvokeListenerInstalled = true;
66
+
67
+ bridgeOn('imports.invoke', async (payload) => {
68
+ const { requestId, registrationId, phase, file } = payload ?? {};
69
+ if (requestId == null || registrationId == null || phase == null) {
70
+ return;
71
+ }
72
+
73
+ console.debug('[import]', 'invoke', {
74
+ registrationId,
75
+ phase,
76
+ fileName: file?.name,
77
+ extension: file?.extension
78
+ });
79
+
80
+ const handler = importHandlersByRegistrationId.get(String(registrationId));
81
+ if (!handler) {
82
+ await bridgeInvoke('imports.invokeComplete', {
83
+ requestId,
84
+ ok: false,
85
+ error: `Unknown import handler registration: ${registrationId}`
86
+ });
87
+ return;
88
+ }
89
+
90
+ try {
91
+ if (phase === 'canImport') {
92
+ const result = Boolean(await handler.canImport(file));
93
+ await bridgeInvoke('imports.invokeComplete', { requestId, ok: true, result });
94
+ return;
95
+ }
96
+ if (phase === 'import') {
97
+ await handler.import(file);
98
+ await bridgeInvoke('imports.invokeComplete', { requestId, ok: true, result: undefined });
99
+ return;
100
+ }
101
+ await bridgeInvoke('imports.invokeComplete', {
102
+ requestId,
103
+ ok: false,
104
+ error: `Unsupported import handler phase: ${String(phase)}`
105
+ });
106
+ } catch (error) {
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ await bridgeInvoke('imports.invokeComplete', {
109
+ requestId,
110
+ ok: false,
111
+ error: message
112
+ });
113
+ }
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Clears import handler state — test helper only.
119
+ */
120
+ export function resetImportHandlersForTests() {
121
+ importHandlersByRegistrationId.clear();
122
+ importRegistrationCounter = 0;
123
+ importInvokeListenerInstalled = false;
124
+ }
125
+
19
126
  /**
20
127
  * Parses a view-host role string into agent vs view contribution id.
21
128
  *
@@ -266,6 +373,7 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
266
373
  return;
267
374
  }
268
375
  if (ownerId === HOST_COMMAND_OWNER) {
376
+ console.debug('[import]', 'commands.execute', { commandId, args });
269
377
  await bridgeInvoke('commands.execute', { pluginId: ownerId, commandId, args });
270
378
  return;
271
379
  }
@@ -678,6 +786,36 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
678
786
  assertUi();
679
787
  await bridgeInvoke('host.clearResponse');
680
788
  }
789
+ },
790
+ imports: {
791
+ registerHandler: (extensions, handler) => {
792
+ assertUi();
793
+ if (!isAgent) {
794
+ return noopDisposable();
795
+ }
796
+ const normalizedExtensions = normalizeImportExtensions(extensions);
797
+ if (normalizedExtensions.length === 0) {
798
+ throw new Error(
799
+ 'At least one file extension is required for import handler registration.'
800
+ );
801
+ }
802
+ const registrationId = String(++importRegistrationCounter);
803
+ importHandlersByRegistrationId.set(registrationId, handler);
804
+ console.debug('[import]', 'registerHandler', {
805
+ registrationId,
806
+ extensions: normalizedExtensions
807
+ });
808
+ void bridgeInvoke('imports.registerHandler', {
809
+ registrationId,
810
+ extensions: normalizedExtensions
811
+ });
812
+ return {
813
+ dispose: () => {
814
+ importHandlersByRegistrationId.delete(registrationId);
815
+ void bridgeInvoke('imports.unregisterHandler', { registrationId });
816
+ }
817
+ };
818
+ }
681
819
  }
682
820
  };
683
821
  }
@@ -2,6 +2,7 @@ import { clearContributionRegistry } from './contributionRegistry.js';
2
2
  import {
3
3
  createBridgedPluginContext,
4
4
  executeLocalPluginCommand,
5
+ installImportInvokeListener,
5
6
  mountContributionView,
6
7
  parseViewHostRole,
7
8
  resolveContributionKindFromUrl
@@ -69,6 +70,10 @@ export async function bootstrapViewHost(options = {}) {
69
70
  manifest
70
71
  });
71
72
 
73
+ if (parsedRole.mode === 'agent') {
74
+ installImportInvokeListener();
75
+ }
76
+
72
77
  await module.activate(hc);
73
78
 
74
79
  if (parsedRole.mode === 'agent') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborclient/sdk",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "description": "TypeScript SDK for HarborClient development.",
5
5
  "keywords": [
6
6
  "harborclient",