@kb-labs/studio-hooks 0.2.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.
@@ -0,0 +1,457 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { EventHandler } from '@kb-labs/studio-event-bus';
4
+ import { MutateOptions } from '@tanstack/react-query';
5
+ import { theme, ThemeConfig } from 'antd';
6
+ import { colors, spacing, typography, radius, shadows } from '@kb-labs/studio-ui-core';
7
+
8
+ /**
9
+ * Page context — provided to every plugin page by the host.
10
+ */
11
+ interface PageContext {
12
+ /** Page ID from the manifest */
13
+ pageId: string;
14
+ /** Plugin that owns this page */
15
+ pluginId: string;
16
+ /** Additional config passed to this page instance */
17
+ config: Record<string, unknown>;
18
+ /** User permissions applicable to this page */
19
+ permissions: string[];
20
+ }
21
+ interface PageContextProviderProps {
22
+ children: ReactNode;
23
+ value: PageContext;
24
+ }
25
+ declare function PageContextProvider({ children, value }: PageContextProviderProps): react_jsx_runtime.JSX.Element;
26
+ declare function usePageContext(): PageContext;
27
+
28
+ /**
29
+ * Access the current page context.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * const { pageId, pluginId, permissions } = usePage();
34
+ * ```
35
+ */
36
+ declare function usePage(): PageContext;
37
+
38
+ interface UseEventBusReturn {
39
+ /** Publish an event. pluginId/pageId auto-injected from context. */
40
+ publish: <T = unknown>(event: string, payload: T) => void;
41
+ /** Subscribe to an event. Auto-unsubscribes on unmount. Returns manual unsubscribe fn. */
42
+ subscribe: <T = unknown>(event: string, handler: EventHandler<T>) => () => void;
43
+ }
44
+ /**
45
+ * EventBus hook for cross-plugin communication.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * const bus = useEventBus();
50
+ * bus.publish('commit:completed', { scope });
51
+ * bus.subscribe('quality:scanDone', (payload) => { ... });
52
+ * ```
53
+ */
54
+ declare function useEventBus(): UseEventBusReturn;
55
+
56
+ interface UseDataOptions<T> {
57
+ /** Polling interval in ms (0 = no polling) */
58
+ pollingMs?: number;
59
+ /** Enable/disable the query */
60
+ enabled?: boolean;
61
+ /** Stale time override in ms */
62
+ staleTime?: number;
63
+ /** Transform response before returning */
64
+ select?: (data: unknown) => T;
65
+ /** Additional query params appended to endpoint */
66
+ params?: Record<string, string | number | boolean>;
67
+ }
68
+ interface UseDataReturn<T> {
69
+ data: T | undefined;
70
+ isLoading: boolean;
71
+ isError: boolean;
72
+ error: Error | null;
73
+ refetch: () => Promise<unknown>;
74
+ isFetching: boolean;
75
+ }
76
+ /**
77
+ * Data fetching hook wrapping TanStack Query.
78
+ * Fetches from the platform REST API (same origin).
79
+ *
80
+ * @example
81
+ * ```tsx
82
+ * const { data, isLoading } = useData<Commit[]>('/v1/plugins/commit/history');
83
+ * const { data: filtered } = useData('/v1/plugins/commit/files', { params: { scope } });
84
+ * ```
85
+ */
86
+ declare function useData<T = unknown>(endpoint: string, options?: UseDataOptions<T>): UseDataReturn<T>;
87
+ interface UseMutateDataReturn<TInput, TOutput> {
88
+ mutate: (input: TInput, options?: MutateOptions<TOutput, Error, TInput>) => void;
89
+ mutateAsync: (input: TInput) => Promise<TOutput>;
90
+ isLoading: boolean;
91
+ isError: boolean;
92
+ error: Error | null;
93
+ data: TOutput | undefined;
94
+ }
95
+ /**
96
+ * Mutation hook for POST/PUT/PATCH/DELETE.
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * const { mutateAsync } = useMutateData<CommitInput, CommitResult>('/v1/plugins/commit/create');
101
+ * await mutateAsync({ scope, message });
102
+ * ```
103
+ */
104
+ declare function useMutateData<TInput = unknown, TOutput = unknown>(endpoint: string, method?: 'POST' | 'PUT' | 'PATCH' | 'DELETE'): UseMutateDataReturn<TInput, TOutput>;
105
+
106
+ interface UseSSEOptions<T> {
107
+ /** SSE event name(s) to listen on (default: 'message') */
108
+ events?: string | string[];
109
+ /** Event that signals the stream is done (auto-closes connection) */
110
+ doneEvent?: string;
111
+ /** Enable/disable the hook (default: true) */
112
+ enabled?: boolean;
113
+ /** Auto-reconnect on error (default: false) */
114
+ reconnect?: boolean;
115
+ /** Max reconnect attempts (default: 5) */
116
+ maxReconnects?: number;
117
+ /** Base reconnect delay in ms, doubles each attempt (default: 2000) */
118
+ reconnectIntervalMs?: number;
119
+ /** Max events to keep in buffer (0 = unlimited, default: 0) */
120
+ maxEvents?: number;
121
+ /** Parse event data (default: JSON.parse) */
122
+ parse?: (raw: string) => T;
123
+ /** Query params appended to endpoint */
124
+ params?: Record<string, string | number | boolean>;
125
+ /** Called on each incoming event */
126
+ onEvent?: (event: T) => void;
127
+ /** Called on connection open */
128
+ onOpen?: () => void;
129
+ /** Called on error (before reconnect) */
130
+ onError?: (error: Error) => void;
131
+ /** Called when done event received or connection closed cleanly */
132
+ onDone?: () => void;
133
+ }
134
+ interface UseSSEReturn<T> {
135
+ /** Accumulated events */
136
+ events: T[];
137
+ /** Latest event received */
138
+ latest: T | undefined;
139
+ /** Whether EventSource is connected */
140
+ isConnected: boolean;
141
+ /** Last error */
142
+ error: Error | null;
143
+ /** Manually close the connection */
144
+ close: () => void;
145
+ /** Clear the event buffer */
146
+ clear: () => void;
147
+ /** Current reconnect attempt count */
148
+ reconnectCount: number;
149
+ }
150
+ /**
151
+ * Generic SSE hook for server-sent event streams.
152
+ *
153
+ * Pass `null` as endpoint to keep the hook mounted but inactive.
154
+ *
155
+ * @example
156
+ * ```tsx
157
+ * // Workflow logs
158
+ * const { events, isConnected } = useSSE<LogEvent>(
159
+ * runId ? `/workflows/runs/${runId}/logs` : null,
160
+ * { events: 'workflow.log', doneEvent: 'workflow.done', params: { follow: '1' } },
161
+ * );
162
+ *
163
+ * // Observability log stream with reconnect
164
+ * const { events, latest } = useSSE<LogRecord>('/logs/stream', {
165
+ * params: { level: 'error' },
166
+ * maxEvents: 500,
167
+ * reconnect: true,
168
+ * });
169
+ * ```
170
+ */
171
+ declare function useSSE<T = unknown>(endpoint: string | null, options?: UseSSEOptions<T>): UseSSEReturn<T>;
172
+
173
+ interface UseInfiniteDataOptions<T, TCursor = string> {
174
+ /** Extract the next-page cursor from the last fetched page. Return undefined to signal "no more pages". */
175
+ getNextCursor: (lastPage: T) => TCursor | undefined;
176
+ /** Additional query params (merged with cursor on each request) */
177
+ params?: Record<string, string | number | boolean>;
178
+ /** Enable/disable the query (default: true) */
179
+ enabled?: boolean;
180
+ /** Stale time override in ms */
181
+ staleTime?: number;
182
+ /** Query param name used to pass the cursor (default: 'cursor') */
183
+ cursorParam?: string;
184
+ /** Initial cursor value (default: undefined → first page) */
185
+ initialCursor?: TCursor;
186
+ }
187
+ interface UseInfiniteDataReturn<T> {
188
+ /** All fetched pages */
189
+ pages: T[];
190
+ /** Flattened data from all pages (requires pages to be arrays) */
191
+ data: T extends Array<infer U> ? U[] : T[];
192
+ /** Loading first page */
193
+ isLoading: boolean;
194
+ /** Fetching any page (initial or next) */
195
+ isFetching: boolean;
196
+ /** Currently fetching next page */
197
+ isFetchingNextPage: boolean;
198
+ /** Whether more pages are available */
199
+ hasNextPage: boolean;
200
+ /** Fetch the next page */
201
+ fetchNextPage: () => Promise<unknown>;
202
+ /** Error if any */
203
+ error: Error | null;
204
+ isError: boolean;
205
+ /** Refetch all pages */
206
+ refetch: () => Promise<unknown>;
207
+ }
208
+ /**
209
+ * Infinite/cursor-based pagination hook wrapping TanStack useInfiniteQuery.
210
+ *
211
+ * Works with any cursor-based or offset-based REST endpoint.
212
+ *
213
+ * @example
214
+ * ```tsx
215
+ * // Cursor-based pagination
216
+ * const { data, fetchNextPage, hasNextPage } = useInfiniteData<RunsPage>(
217
+ * '/workflows/runs',
218
+ * {
219
+ * getNextCursor: (page) => page.nextCursor,
220
+ * params: { status: 'running' },
221
+ * },
222
+ * );
223
+ *
224
+ * // Offset-based pagination
225
+ * const { data, fetchNextPage } = useInfiniteData<LogEntry[]>(
226
+ * '/logs',
227
+ * {
228
+ * cursorParam: 'offset',
229
+ * getNextCursor: (page) => page.length === 50 ? String(allLogs.length) : undefined,
230
+ * },
231
+ * );
232
+ * ```
233
+ */
234
+ declare function useInfiniteData<T = unknown, TCursor = string>(endpoint: string, options: UseInfiniteDataOptions<T, TCursor>): UseInfiniteDataReturn<T>;
235
+
236
+ type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
237
+ interface UseWebSocketOptions<TReceive> {
238
+ /** Enable/disable auto-connect on mount (default: true) */
239
+ enabled?: boolean;
240
+ /** Auto-reconnect on close/error (default: false) */
241
+ reconnect?: boolean;
242
+ /** Max reconnect attempts (default: 5) */
243
+ maxReconnects?: number;
244
+ /** Base reconnect delay in ms, doubles each attempt (default: 2000) */
245
+ reconnectIntervalMs?: number;
246
+ /** WebSocket sub-protocols */
247
+ protocols?: string | string[];
248
+ /** Parse incoming messages (default: JSON.parse) */
249
+ parse?: (raw: string) => TReceive;
250
+ /** Serialize outgoing messages (default: JSON.stringify) */
251
+ serialize?: (data: unknown) => string;
252
+ /** Max messages to keep in history (default: 100) */
253
+ maxMessages?: number;
254
+ /** Called on each received message */
255
+ onMessage?: (data: TReceive) => void;
256
+ /** Called on connection open */
257
+ onOpen?: (event: Event) => void;
258
+ /** Called on connection close */
259
+ onClose?: (event: CloseEvent) => void;
260
+ /** Called on error */
261
+ onError?: (event: Event) => void;
262
+ }
263
+ interface UseWebSocketReturn<TSend, TReceive> {
264
+ /** Send a typed message (auto-serialized) */
265
+ send: (data: TSend) => void;
266
+ /** Send a raw string */
267
+ sendRaw: (data: string | ArrayBufferLike | Blob) => void;
268
+ /** Received messages (most recent last) */
269
+ messages: TReceive[];
270
+ /** Latest received message */
271
+ latest: TReceive | undefined;
272
+ /** Connection status */
273
+ status: WebSocketStatus;
274
+ /** Shorthand: status === 'connected' */
275
+ isConnected: boolean;
276
+ /** Last error event */
277
+ error: Event | null;
278
+ /** Manually open the connection */
279
+ connect: () => void;
280
+ /** Manually close the connection */
281
+ disconnect: (code?: number, reason?: string) => void;
282
+ /** Clear message history */
283
+ clear: () => void;
284
+ /** Current reconnect attempt count */
285
+ reconnectCount: number;
286
+ }
287
+ /**
288
+ * Generic WebSocket hook for bidirectional communication.
289
+ *
290
+ * Pass `null` as url to keep the hook mounted but inactive.
291
+ *
292
+ * @example
293
+ * ```tsx
294
+ * // Agent chat
295
+ * const { send, messages, isConnected } = useWebSocket<UserMsg, AgentMsg>(
296
+ * sessionId ? `ws://localhost:4000/agent/${sessionId}` : null,
297
+ * );
298
+ * send({ type: 'user_message', text: input });
299
+ *
300
+ * // With reconnect and callbacks
301
+ * const { messages, status } = useWebSocket<Command, StatusEvent>(
302
+ * '/ws/deploy',
303
+ * {
304
+ * reconnect: true,
305
+ * onMessage: (msg) => msg.done && showNotification('Deploy finished'),
306
+ * },
307
+ * );
308
+ * ```
309
+ */
310
+ declare function useWebSocket<TSend = unknown, TReceive = unknown>(url: string | null, options?: UseWebSocketOptions<TReceive>): UseWebSocketReturn<TSend, TReceive>;
311
+
312
+ interface UsePermissionsReturn {
313
+ /** Check if the current user has a specific permission */
314
+ hasPermission: (permission: string) => boolean;
315
+ /** All permissions available to this page */
316
+ permissions: string[];
317
+ }
318
+ /**
319
+ * Atomic permission checks inside a page.
320
+ * Page-level permissions are enforced by the host before loading.
321
+ * This hook is for fine-grained control within the page.
322
+ *
323
+ * @example
324
+ * ```tsx
325
+ * const { hasPermission } = usePermissions();
326
+ * if (!hasPermission('commit:write')) return <ReadOnlyView />;
327
+ * ```
328
+ */
329
+ declare function usePermissions(): UsePermissionsReturn;
330
+
331
+ interface UseNavigationReturn {
332
+ /** Navigate to a route */
333
+ navigate: (path: string) => void;
334
+ /** Current pathname */
335
+ currentPath: string;
336
+ /** Go back in history */
337
+ goBack: () => void;
338
+ /** Open external URL in new tab */
339
+ openExternal: (url: string) => void;
340
+ }
341
+ /**
342
+ * Navigation hook for plugin pages.
343
+ *
344
+ * @example
345
+ * ```tsx
346
+ * const { navigate, currentPath } = useNavigation();
347
+ * navigate('/commit/history');
348
+ * ```
349
+ */
350
+ declare function useNavigation(): UseNavigationReturn;
351
+
352
+ type NotificationType = 'success' | 'error' | 'warning' | 'info';
353
+ interface UseNotificationReturn {
354
+ notify: (options: {
355
+ type: NotificationType;
356
+ message: string;
357
+ description?: string;
358
+ duration?: number;
359
+ }) => void;
360
+ success: (message: string, description?: string) => void;
361
+ error: (message: string, description?: string) => void;
362
+ warning: (message: string, description?: string) => void;
363
+ info: (message: string, description?: string) => void;
364
+ }
365
+ /**
366
+ * Notification hook for plugin pages.
367
+ *
368
+ * @example
369
+ * ```tsx
370
+ * const notify = useNotification();
371
+ * notify.success('Commit created');
372
+ * notify.error('Build failed', 'Check logs for details');
373
+ * ```
374
+ */
375
+ declare function useNotification(): UseNotificationReturn;
376
+
377
+ /**
378
+ * Semantic tokens — theme-aware colors for backgrounds, text, borders, status.
379
+ * These mirror the CSS variables injected by Studio host (e.g. `var(--bg-primary)`).
380
+ */
381
+ interface SemanticTokens {
382
+ bgPrimary: string;
383
+ bgSecondary: string;
384
+ bgTertiary: string;
385
+ textPrimary: string;
386
+ textSecondary: string;
387
+ textTertiary: string;
388
+ textInverse: string;
389
+ borderPrimary: string;
390
+ borderSecondary: string;
391
+ link: string;
392
+ linkHover: string;
393
+ accentSubtle: string;
394
+ success: string;
395
+ warning: string;
396
+ error: string;
397
+ info: string;
398
+ disabled: string;
399
+ shadow: string;
400
+ }
401
+ interface UseThemeReturn {
402
+ /** Current theme mode */
403
+ mode: 'light' | 'dark';
404
+ /** Ant Design design token */
405
+ antdToken: ReturnType<typeof theme.useToken>['token'];
406
+ /** KB Labs semantic tokens (theme-aware) */
407
+ semantic: SemanticTokens;
408
+ /** Raw design tokens (colors, spacing, typography, radius, shadows) */
409
+ tokens: {
410
+ colors: typeof colors;
411
+ spacing: typeof spacing;
412
+ typography: typeof typography;
413
+ radius: typeof radius;
414
+ shadows: typeof shadows;
415
+ };
416
+ }
417
+ /**
418
+ * Theme hook for plugin pages.
419
+ *
420
+ * Three levels of access:
421
+ * - `semantic` — high-level themed tokens (bgPrimary, textPrimary, success, error...)
422
+ * - `antdToken` — Ant Design's full design token for antd component customization
423
+ * - `tokens` — raw design tokens (colors, spacing, typography, radius, shadows)
424
+ *
425
+ * CSS variables (`var(--bg-primary)`, `var(--text-primary)`) are also available
426
+ * in stylesheets — injected by the Studio host into `:root`.
427
+ *
428
+ * @example
429
+ * ```tsx
430
+ * const { semantic, tokens, mode } = useTheme();
431
+ *
432
+ * // Semantic tokens (recommended)
433
+ * <div style={{ color: semantic.textPrimary, background: semantic.bgSecondary }}>
434
+ *
435
+ * // CSS variables in stylesheets
436
+ * // .my-component { color: var(--text-primary); }
437
+ *
438
+ * // Raw tokens for specific shades
439
+ * <div style={{ color: tokens.colors.primary[600] }}>
440
+ * ```
441
+ */
442
+ declare function useTheme(): UseThemeReturn;
443
+
444
+ /**
445
+ * Maps CSS variables from ui-core to Ant Design tokens
446
+ * Uses CSS variables directly via var() for automatic theme switching
447
+ * This adapter maintains framework-agnostic nature of ui-core
448
+ * All Ant Design tokens are mapped to our CSS variables for consistency
449
+ */
450
+ declare function getAntDesignTokens(): ThemeConfig['token'];
451
+ /**
452
+ * Gets component-specific theme configurations
453
+ * Uses Ant Design's components API for proper theming without CSS overrides
454
+ */
455
+ declare function getAntDesignComponents(): ThemeConfig['components'];
456
+
457
+ export { type NotificationType, type PageContext, PageContextProvider, type SemanticTokens, type UseDataOptions, type UseDataReturn, type UseEventBusReturn, type UseInfiniteDataOptions, type UseInfiniteDataReturn, type UseMutateDataReturn, type UseNavigationReturn, type UseNotificationReturn, type UsePermissionsReturn, type UseSSEOptions, type UseSSEReturn, type UseThemeReturn, type UseWebSocketOptions, type UseWebSocketReturn, type WebSocketStatus, getAntDesignComponents, getAntDesignTokens, useData, useEventBus, useInfiniteData, useMutateData, useNavigation, useNotification, usePage, usePageContext, usePermissions, useSSE, useTheme, useWebSocket };