@harborclient/sdk 1.5.3 → 1.6.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/components/Breadcrumb/CrumbSegment.d.ts.map +1 -1
  3. package/dist/components/Breadcrumb/CrumbSegment.js +4 -2
  4. package/dist/components/Breadcrumb/types.d.ts +10 -1
  5. package/dist/components/Breadcrumb/types.d.ts.map +1 -1
  6. package/dist/components/MethodSelect/MethodSelectMenu.d.ts +34 -0
  7. package/dist/components/MethodSelect/MethodSelectMenu.d.ts.map +1 -0
  8. package/dist/components/MethodSelect/MethodSelectMenu.js +150 -0
  9. package/dist/components/MethodSelect/MethodSelectOption.d.ts +36 -0
  10. package/dist/components/MethodSelect/MethodSelectOption.d.ts.map +1 -0
  11. package/dist/components/MethodSelect/MethodSelectOption.js +15 -0
  12. package/dist/components/MethodSelect/MethodSelectSeparator.d.ts +10 -0
  13. package/dist/components/MethodSelect/MethodSelectSeparator.d.ts.map +1 -0
  14. package/dist/components/MethodSelect/MethodSelectSeparator.js +11 -0
  15. package/dist/components/MethodSelect/index.d.ts +21 -7
  16. package/dist/components/MethodSelect/index.d.ts.map +1 -1
  17. package/dist/components/MethodSelect/index.js +47 -6
  18. package/dist/components/SidebarItem/sidebarItemClasses.d.ts.map +1 -1
  19. package/dist/components/SidebarItem/sidebarItemClasses.js +2 -1
  20. package/dist/components/index.d.ts +1 -1
  21. package/dist/components/index.d.ts.map +1 -1
  22. package/dist/runtime/createBridgedPluginContext.js +295 -7
  23. package/dist/runtime/viewHost.js +6 -0
  24. package/dist/snippets.d.ts +30 -19
  25. package/dist/types.d.ts +337 -16
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/ui/tokens.d.ts +1 -0
  28. package/dist/ui/tokens.d.ts.map +1 -1
  29. package/dist/ui/tokens.js +4 -2
  30. package/package.json +1 -1
@@ -15,12 +15,41 @@ const commandHandlers = new Map();
15
15
  /** @type {Map<string, import('../types').ImportHandler>} */
16
16
  const importHandlersByRegistrationId = new Map();
17
17
 
18
+ /**
19
+ * Parse callbacks for custom chat pointers, keyed by registration id.
20
+ *
21
+ * @type {Map<string, NonNullable<import('../types').PluginChatPointerConfig['parse']>>}
22
+ */
23
+ const chatPointerParseByRegistrationId = new Map();
24
+
18
25
  /** Monotonic id generator for import handler registrations within one webview. */
19
26
  let importRegistrationCounter = 0;
20
27
 
21
28
  /** Monotonic id generator for MCP server registrations within one webview. */
22
29
  let mcpRegistrationCounter = 0;
23
30
  let aiChatPointerRegistrationCounter = 0;
31
+ let aiInstructionsRegistrationCounter = 0;
32
+
33
+ /**
34
+ * Before-turn handlers registered via `hc.ai.onBeforeTurn`.
35
+ *
36
+ * @type {Set<(ctx: import('../types').PluginAiBeforeTurnContext) => void | Promise<void>>}
37
+ */
38
+ const aiBeforeTurnHandlers = new Set();
39
+
40
+ /**
41
+ * After-turn handlers registered via `hc.ai.onAfterTurn`.
42
+ *
43
+ * @type {Set<(ctx: import('../types').PluginAiAfterTurnContext) => void | Promise<void>>}
44
+ */
45
+ const aiAfterTurnHandlers = new Set();
46
+
47
+ /**
48
+ * Local instruction texts keyed by registration id (for `hc.ai.instructions.list`).
49
+ *
50
+ * @type {Map<string, string>}
51
+ */
52
+ const aiInstructionsByRegistrationId = new Map();
24
53
 
25
54
  /** Plugin id prefix for built-in HarborClient host commands executed in the renderer. */
26
55
  const HOST_COMMAND_OWNER = 'harborclient';
@@ -28,6 +57,15 @@ const HOST_COMMAND_OWNER = 'harborclient';
28
57
  /** Guards repeated import invoke listener installation per webview load. */
29
58
  let importInvokeListenerInstalled = false;
30
59
 
60
+ /** Guards repeated chat-pointer parse listener installation per webview load. */
61
+ let aiParseChatPointerListenerInstalled = false;
62
+
63
+ /** Guards repeated before-turn listener installation per webview load. */
64
+ let aiBeforeTurnListenerInstalled = false;
65
+
66
+ /** Guards repeated after-turn listener installation per webview load. */
67
+ let aiAfterTurnListenerInstalled = false;
68
+
31
69
  /**
32
70
  * Normalizes a file extension to lowercase with a leading dot.
33
71
  *
@@ -123,6 +161,11 @@ export function installImportInvokeListener() {
123
161
  export function resetMcpServersForTests() {
124
162
  mcpRegistrationCounter = 0;
125
163
  aiChatPointerRegistrationCounter = 0;
164
+ aiInstructionsRegistrationCounter = 0;
165
+ chatPointerParseByRegistrationId.clear();
166
+ aiInstructionsByRegistrationId.clear();
167
+ aiBeforeTurnHandlers.clear();
168
+ aiAfterTurnHandlers.clear();
126
169
  }
127
170
 
128
171
  /**
@@ -132,9 +175,183 @@ export function resetImportHandlersForTests() {
132
175
  importHandlersByRegistrationId.clear();
133
176
  importRegistrationCounter = 0;
134
177
  importInvokeListenerInstalled = false;
178
+ aiParseChatPointerListenerInstalled = false;
179
+ aiBeforeTurnListenerInstalled = false;
180
+ aiAfterTurnListenerInstalled = false;
135
181
  resetMcpServersForTests();
136
182
  }
137
183
 
184
+ /**
185
+ * Serializes a plugin chat-pointer match for the host bridge.
186
+ *
187
+ * @param {RegExp | string} match - Plugin-supplied match.
188
+ * @returns {{ source: string; flags: string }}
189
+ */
190
+ function serializeChatPointerMatch(match) {
191
+ if (match instanceof RegExp) {
192
+ return { source: match.source, flags: match.flags.replace(/g/g, '') };
193
+ }
194
+ return { source: String(match ?? ''), flags: '' };
195
+ }
196
+
197
+ /**
198
+ * Subscribes to host-initiated chat-pointer parse invocations for the agent webview.
199
+ *
200
+ * Must run once before plugin activation so send/validate can reach registered parsers.
201
+ */
202
+ export function installAiParseChatPointerListener() {
203
+ if (aiParseChatPointerListenerInstalled) {
204
+ return;
205
+ }
206
+ aiParseChatPointerListenerInstalled = true;
207
+
208
+ bridgeOn('ai.parseChatPointer', async (payload) => {
209
+ const { requestId, registrationId, matchGroups, fullToken, atIndex } = payload ?? {};
210
+ if (requestId == null || registrationId == null) {
211
+ return;
212
+ }
213
+
214
+ const parse = chatPointerParseByRegistrationId.get(String(registrationId));
215
+ if (!parse) {
216
+ await bridgeInvoke('ai.parseChatPointerComplete', {
217
+ requestId,
218
+ ok: false,
219
+ error: `Unknown chat pointer parse registration: ${registrationId}`
220
+ });
221
+ return;
222
+ }
223
+
224
+ try {
225
+ const groups = Array.isArray(matchGroups)
226
+ ? matchGroups.map((g) => (g == null ? undefined : String(g)))
227
+ : [];
228
+ const synthetic = /** @type {RegExpMatchArray} */ (groups);
229
+ synthetic.index = 0;
230
+ synthetic.input = String(fullToken ?? '').replace(/^@/, '');
231
+ const result = parse(synthetic, String(fullToken ?? ''), Number(atIndex) || 0);
232
+ await bridgeInvoke('ai.parseChatPointerComplete', {
233
+ requestId,
234
+ ok: true,
235
+ result: result ?? null
236
+ });
237
+ } catch (error) {
238
+ const message = error instanceof Error ? error.message : String(error);
239
+ await bridgeInvoke('ai.parseChatPointerComplete', {
240
+ requestId,
241
+ ok: false,
242
+ error: message
243
+ });
244
+ }
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Subscribes to host-initiated before-turn invocations for the agent webview.
250
+ *
251
+ * Must run once before plugin activation so chat sends can reach registered handlers.
252
+ */
253
+ export function installAiBeforeTurnListener() {
254
+ if (aiBeforeTurnListenerInstalled) {
255
+ return;
256
+ }
257
+ aiBeforeTurnListenerInstalled = true;
258
+
259
+ bridgeOn('ai.beforeTurn', async (payload) => {
260
+ const { requestId, chatId, model, hubId, userMessage, messages } = payload ?? {};
261
+ if (requestId == null) {
262
+ return;
263
+ }
264
+
265
+ try {
266
+ const extraInstructions = [];
267
+ let cancelled = false;
268
+ let cancelReason;
269
+ const userMessageState = {
270
+ content: String(userMessage?.content ?? ''),
271
+ ...(userMessage?.referenceSnapshots != null
272
+ ? { referenceSnapshots: userMessage.referenceSnapshots }
273
+ : {})
274
+ };
275
+ /** @type {import('../types').PluginAiBeforeTurnContext} */
276
+ const ctx = {
277
+ chatId: Number(chatId) || 0,
278
+ model: String(model ?? ''),
279
+ ...(hubId != null && String(hubId).trim() !== '' ? { hubId: String(hubId) } : {}),
280
+ userMessage: userMessageState,
281
+ instructions: {
282
+ push: (text) => {
283
+ const trimmed = String(text ?? '').trim();
284
+ if (trimmed) {
285
+ extraInstructions.push(trimmed);
286
+ }
287
+ },
288
+ get list() {
289
+ return [...extraInstructions];
290
+ }
291
+ },
292
+ messages: Array.isArray(messages)
293
+ ? messages.map((row) => ({
294
+ role: row?.role ?? 'user',
295
+ content: row?.content ?? null
296
+ }))
297
+ : [],
298
+ cancel: (reason) => {
299
+ cancelled = true;
300
+ if (reason != null && String(reason).trim() !== '') {
301
+ cancelReason = String(reason).trim();
302
+ }
303
+ }
304
+ };
305
+
306
+ for (const handler of [...aiBeforeTurnHandlers]) {
307
+ await handler(ctx);
308
+ if (cancelled) {
309
+ break;
310
+ }
311
+ }
312
+
313
+ await bridgeInvoke('ai.beforeTurnComplete', {
314
+ requestId,
315
+ ok: true,
316
+ result: {
317
+ cancelled,
318
+ ...(cancelReason != null ? { cancelReason } : {}),
319
+ userContent: ctx.userMessage.content,
320
+ extraInstructions
321
+ }
322
+ });
323
+ } catch (error) {
324
+ const message = error instanceof Error ? error.message : String(error);
325
+ await bridgeInvoke('ai.beforeTurnComplete', {
326
+ requestId,
327
+ ok: false,
328
+ error: message
329
+ });
330
+ }
331
+ });
332
+ }
333
+
334
+ /**
335
+ * Subscribes to host-initiated after-turn push events for the agent webview.
336
+ *
337
+ * Must run once before plugin activation.
338
+ */
339
+ export function installAiAfterTurnListener() {
340
+ if (aiAfterTurnListenerInstalled) {
341
+ return;
342
+ }
343
+ aiAfterTurnListenerInstalled = true;
344
+
345
+ bridgeOn('ai.afterTurn', (payload) => {
346
+ const ctx = /** @type {import('../types').PluginAiAfterTurnContext} */ (payload);
347
+ for (const handler of [...aiAfterTurnHandlers]) {
348
+ void Promise.resolve(handler(ctx)).catch((error) => {
349
+ console.error('Plugin ai.onAfterTurn handler failed:', error);
350
+ });
351
+ }
352
+ });
353
+ }
354
+
138
355
  /**
139
356
  * Normalizes one MCP header row from plugin registration input.
140
357
  *
@@ -338,6 +555,11 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
338
555
  */
339
556
  const assertUi = () => assertPermission('ui');
340
557
 
558
+ /**
559
+ * Asserts network permission for outbound HTTP via hc.host.fetch.
560
+ */
561
+ const assertNetwork = () => assertPermission('network');
562
+
341
563
  /**
342
564
  * Returns whether UI registration should run in this webview role.
343
565
  */
@@ -1080,9 +1302,9 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
1080
1302
  }
1081
1303
  });
1082
1304
  },
1083
- sendRequest: async () => {
1305
+ send: async () => {
1084
1306
  assertUi();
1085
- await bridgeInvoke('host.sendRequest');
1307
+ await bridgeInvoke('host.send');
1086
1308
  },
1087
1309
  createEnvironmentWithVariables: async (name, variables) => {
1088
1310
  assertUi();
@@ -1254,9 +1476,9 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
1254
1476
  assertUi();
1255
1477
  await bridgeInvoke('host.logRequestToConsole', { payload });
1256
1478
  },
1257
- sendHttpRequest: async (input) => {
1258
- assertUi();
1259
- return bridgeInvoke('host.sendHttpRequest', { input });
1479
+ fetch: async (input, init) => {
1480
+ assertNetwork();
1481
+ return bridgeInvoke('host.fetch', { input, init });
1260
1482
  },
1261
1483
  clearResponse: async () => {
1262
1484
  assertUi();
@@ -1408,13 +1630,28 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
1408
1630
  if (!/^[a-z][a-z0-9-]*$/.test(pointerId)) {
1409
1631
  throw new Error(`Invalid chat pointer id: ${pointerId}`);
1410
1632
  }
1633
+ const hasMatch = config?.match != null && config.match !== '';
1634
+ const hasParse = typeof config?.parse === 'function';
1635
+ if (hasMatch !== hasParse) {
1636
+ throw new Error('Chat pointer match and parse must be provided together.');
1637
+ }
1638
+
1411
1639
  const registrationId = String(++aiChatPointerRegistrationCounter);
1640
+ /** @type {{ source: string; flags: string } | undefined} */
1641
+ let matchPayload;
1642
+ if (hasMatch) {
1643
+ matchPayload = serializeChatPointerMatch(/** @type {RegExp | string} */ (config.match));
1644
+ chatPointerParseByRegistrationId.set(registrationId, config.parse);
1645
+ }
1646
+
1412
1647
  void bridgeInvoke('ai.registerChatPointer', {
1413
1648
  registrationId,
1414
1649
  pointerId,
1415
- agentGuidance: config?.agentGuidance
1650
+ agentGuidance: config?.agentGuidance,
1651
+ ...(matchPayload != null ? { match: matchPayload } : {})
1416
1652
  });
1417
1653
  return track(() => {
1654
+ chatPointerParseByRegistrationId.delete(registrationId);
1418
1655
  void bridgeInvoke('ai.unregisterChatPointer', { registrationId });
1419
1656
  });
1420
1657
  },
@@ -1422,11 +1659,62 @@ export function createBridgedPluginContext({ pluginId, mode, contributionId, rea
1422
1659
  assertAi();
1423
1660
  await bridgeInvoke('ai.copyToChat', {
1424
1661
  pointerId: String(input?.pointerId ?? '').trim(),
1425
- key: String(input?.key ?? '').trim(),
1662
+ key: input?.key != null ? String(input.key).trim() : undefined,
1663
+ token: input?.token != null ? String(input.token).trim() : undefined,
1426
1664
  label: String(input?.label ?? '').trim(),
1427
1665
  context: String(input?.context ?? ''),
1428
1666
  selection: input?.selection
1429
1667
  });
1668
+ },
1669
+ instructions: {
1670
+ add: (text) => {
1671
+ assertAi();
1672
+ if (!isAgent) {
1673
+ return noopDisposable();
1674
+ }
1675
+ const trimmed = String(text ?? '').trim();
1676
+ const registrationId = String(++aiInstructionsRegistrationCounter);
1677
+ if (trimmed) {
1678
+ aiInstructionsByRegistrationId.set(registrationId, trimmed);
1679
+ }
1680
+ void bridgeInvoke('ai.registerInstructions', {
1681
+ registrationId,
1682
+ text: trimmed
1683
+ });
1684
+ return track(() => {
1685
+ aiInstructionsByRegistrationId.delete(registrationId);
1686
+ void bridgeInvoke('ai.unregisterInstructions', { registrationId });
1687
+ });
1688
+ },
1689
+ get list() {
1690
+ return [...aiInstructionsByRegistrationId.values()];
1691
+ }
1692
+ },
1693
+ onBeforeTurn: (handler) => {
1694
+ assertAi();
1695
+ if (!isAgent) {
1696
+ return noopDisposable();
1697
+ }
1698
+ if (typeof handler !== 'function') {
1699
+ throw new Error('onBeforeTurn handler must be a function.');
1700
+ }
1701
+ aiBeforeTurnHandlers.add(handler);
1702
+ return track(() => {
1703
+ aiBeforeTurnHandlers.delete(handler);
1704
+ });
1705
+ },
1706
+ onAfterTurn: (handler) => {
1707
+ assertAi();
1708
+ if (!isAgent) {
1709
+ return noopDisposable();
1710
+ }
1711
+ if (typeof handler !== 'function') {
1712
+ throw new Error('onAfterTurn handler must be a function.');
1713
+ }
1714
+ aiAfterTurnHandlers.add(handler);
1715
+ return track(() => {
1716
+ aiAfterTurnHandlers.delete(handler);
1717
+ });
1430
1718
  }
1431
1719
  },
1432
1720
  /**
@@ -2,6 +2,9 @@ import { clearContributionRegistry } from './contributionRegistry.js';
2
2
  import {
3
3
  createBridgedPluginContext,
4
4
  executeLocalPluginCommand,
5
+ installAiAfterTurnListener,
6
+ installAiBeforeTurnListener,
7
+ installAiParseChatPointerListener,
5
8
  installImportInvokeListener,
6
9
  mountContributionView,
7
10
  parseViewHostRole,
@@ -72,6 +75,9 @@ export async function bootstrapViewHost(options = {}) {
72
75
 
73
76
  if (parsedRole.mode === 'agent') {
74
77
  installImportInvokeListener();
78
+ installAiParseChatPointerListener();
79
+ installAiBeforeTurnListener();
80
+ installAiAfterTurnListener();
75
81
  }
76
82
 
77
83
  await module.activate(hc);
@@ -246,28 +246,37 @@ interface HcInfoApi {
246
246
  }
247
247
 
248
248
  /**
249
- * Outbound request payload accepted by hc.sendRequest.
249
+ * RequestInit-compatible options accepted by hc.fetch.
250
250
  */
251
- interface HcSendRequestInput {
251
+ interface HcFetchInit {
252
252
  method?: string;
253
- url: string;
254
- headers?: Array<{ key: string; value: string; enabled?: boolean }> | Record<string, string>;
255
- params?: Array<{ key: string; value: string; enabled?: boolean }>;
256
- body?: string;
257
- bodyType?: 'none' | 'json' | 'text' | 'multipart' | 'urlencoded';
258
- body_type?: 'none' | 'json' | 'text' | 'multipart' | 'urlencoded';
253
+ headers?: Record<string, string> | Array<[string, string]>;
254
+ body?: string | URLSearchParams | null;
259
255
  }
260
256
 
261
257
  /**
262
- * Response snapshot returned by hc.sendRequest.
258
+ * Headers-like facade returned by hc.fetch.
263
259
  */
264
- interface HcSendRequestResponse {
265
- readonly code: number;
266
- readonly status: string;
267
- readonly headers: Record<string, string>;
268
- readonly responseTime: number;
269
- text(): string;
270
- json(): unknown;
260
+ interface HcFetchHeaders {
261
+ get(name: string): string | null;
262
+ has(name: string): boolean;
263
+ entries(): IterableIterator<[string, string]>;
264
+ keys(): IterableIterator<string>;
265
+ values(): IterableIterator<string>;
266
+ forEach(callback: (value: string, key: string) => void): void;
267
+ }
268
+
269
+ /**
270
+ * Response-compatible object returned by hc.fetch.
271
+ */
272
+ interface HcFetchResponse {
273
+ readonly ok: boolean;
274
+ readonly status: number;
275
+ readonly statusText: string;
276
+ readonly headers: HcFetchHeaders;
277
+ text(): Promise<string>;
278
+ json(): Promise<unknown>;
279
+ arrayBuffer(): Promise<ArrayBuffer>;
271
280
  }
272
281
 
273
282
  /**
@@ -468,12 +477,14 @@ interface HcScriptApi {
468
477
  /** Read-only metadata about the current script run (Postman pm.info equivalent). */
469
478
  info: HcInfoApi;
470
479
  /**
471
- * Sends an outbound HTTP request from the script sandbox.
480
+ * Sends an outbound HTTP request using the native fetch(input, init?) signature.
472
481
  * Requires Settings → General → Allow script network requests.
473
482
  *
474
- * @throws When the setting is disabled or sendRequest is unavailable in this context.
483
+ * @param input - URL string, URL, or Request-like `{ url }` object.
484
+ * @param init - Optional RequestInit-compatible options.
485
+ * @throws When the setting is disabled or fetch is unavailable in this context.
475
486
  */
476
- sendRequest(req: HcSendRequestInput): Promise<HcSendRequestResponse>;
487
+ fetch(input: string | URL | { url: string }, init?: HcFetchInit): Promise<HcFetchResponse>;
477
488
  /**
478
489
  * Sends a one-shot prompt to a configured AI model.
479
490
  * Includes the current send's request (and response in post-request scripts)