@m6d/cortex-angular 0.1.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/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@m6d/cortex-angular",
3
+ "version": "0.1.0",
4
+ "description": "Reusable AI agent chat UI library for Angular",
5
+ "license": "MIT",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ "./theme.css": "./theme.css",
9
+ "./package.json": {
10
+ "default": "./package.json"
11
+ },
12
+ ".": {
13
+ "types": "./types/m6d-cortex-angular.d.ts",
14
+ "default": "./fesm2022/m6d-cortex-angular.mjs"
15
+ }
16
+ },
17
+ "dependencies": {
18
+ "tslib": "^2.8.1"
19
+ },
20
+ "peerDependencies": {
21
+ "@angular/common": ">=21.2.0 <23",
22
+ "@angular/core": ">=21.2.0 <23",
23
+ "@angular/platform-browser": ">=21.2.0 <23",
24
+ "@tanstack/ai": ">=0.55.0 <1.0.0",
25
+ "@tanstack/ai-angular": ">=0.11.0 <1.0.0",
26
+ "@tanstack/ai-client": ">=0.32.0 <1.0.0",
27
+ "dompurify": "^3.0.0",
28
+ "highlight.js": "^11.0.0",
29
+ "leaflet": "^1.9.0",
30
+ "marked": "^17.0.0",
31
+ "mermaid": "^12.0.0",
32
+ "rxjs": "^7.8.0",
33
+ "vega": "^6.0.0",
34
+ "vega-embed": "^7.0.0",
35
+ "vega-lite": "^6.0.0"
36
+ },
37
+ "packageManager": "bun@1.3.3",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "releaseWatchPaths": [
42
+ "internal/client",
43
+ "internal/contracts"
44
+ ],
45
+ "module": "fesm2022/m6d-cortex-angular.mjs",
46
+ "typings": "types/m6d-cortex-angular.d.ts",
47
+ "type": "module"
48
+ }
package/theme.css ADDED
@@ -0,0 +1,71 @@
1
+ /*
2
+ * Theming surface.
3
+ *
4
+ * Every value below is the widget's own default; the compiled stylesheet ships
5
+ * this same file, so importing it changes nothing until you edit it. Override
6
+ * any token on :root, or on any ancestor of the widget, to retheme it.
7
+ *
8
+ * Each color is a light-dark() pair. Which side applies follows the widget root's
9
+ * color-scheme, set by the `theme` config option ('light' | 'dark' | 'system').
10
+ * The radius and font tokens below the colors are plain values — neither depends
11
+ * on the scheme.
12
+ */
13
+ :root {
14
+ --cortex-color-surface: light-dark(#ffffff, #0a0a0a);
15
+ --cortex-color-surface-subtle: light-dark(#f8fafc, #141414);
16
+ --cortex-color-surface-muted: light-dark(#f1f5f9, #1f1f1f);
17
+
18
+ --cortex-color-border: light-dark(#e2e8f0, #2e2e2e);
19
+ --cortex-color-border-strong: light-dark(#cbd5e1, #444444);
20
+
21
+ --cortex-color-text-heading: light-dark(#1e293b, #fafafa);
22
+ --cortex-color-text-strong: light-dark(#334155, #e5e5e5);
23
+ --cortex-color-text: light-dark(#475569, #d4d4d4);
24
+ --cortex-color-text-secondary: light-dark(#64748b, #a3a3a3);
25
+ --cortex-color-text-muted: light-dark(#94a3b8, #737373);
26
+
27
+ --cortex-color-primary: light-dark(#1e293b, #fafafa);
28
+ --cortex-color-primary-foreground: light-dark(#ffffff, #0a0a0a);
29
+ --cortex-color-accent: light-dark(#4f46e5, #818cf8);
30
+ --cortex-color-accent-surface: light-dark(#e0e7ff, #312e81);
31
+ --cortex-color-link: light-dark(#2563eb, #60a5fa);
32
+ --cortex-color-hover: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.06));
33
+
34
+ /* User messages default to the same subtle surface every other quiet panel
35
+ * uses, split out so a consumer can give their own side of the chat a color
36
+ * without touching anything else. */
37
+ --cortex-color-user-surface: var(--cortex-color-surface-subtle);
38
+ --cortex-color-user-border: var(--cortex-color-border);
39
+
40
+ /* Code blocks sit a step below the bubble they are in, in either scheme. */
41
+ --cortex-color-code-surface: light-dark(#e8edf3, #111111);
42
+ --cortex-color-code-text: light-dark(#334155, #e5e5e5);
43
+
44
+ --cortex-color-success: light-dark(#059669, #34d399);
45
+ --cortex-color-success-surface: light-dark(#ecfdf5, #064e3b);
46
+ --cortex-color-success-border: light-dark(#d1fae5, #065f46);
47
+ --cortex-color-danger: light-dark(#dc2626, #f87171);
48
+ --cortex-color-danger-surface: light-dark(#fef2f2, #450a0a);
49
+ --cortex-color-danger-border: light-dark(#fecaca, #7f1d1d);
50
+
51
+ /* The corner scale every rounded surface derives from: the widget multiplies
52
+ * this by 0.5/0.75/1.5/2, so overriding it rescales all of them together. */
53
+ --cortex-radius: 0.5rem;
54
+
55
+ --cortex-font-mono: var(
56
+ --default-mono-font-family,
57
+ ui-monospace,
58
+ SFMono-Regular,
59
+ Menlo,
60
+ Monaco,
61
+ Consolas,
62
+ monospace
63
+ );
64
+ --cortex-font-size: 0.8125rem;
65
+
66
+ /* The widget's headings read --cortex-font-display and fall back to `inherit`,
67
+ * so it ships unset on purpose — declaring it here is how you opt in:
68
+ *
69
+ * --cortex-font-display: 'Your Display Face', sans-serif;
70
+ */
71
+ }
@@ -0,0 +1,412 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ import * as i0 from '@angular/core';
4
+ import { Injector, OnInit, Signal, Type } from '@angular/core';
5
+ import { ToolCallPart, UIMessage } from '@tanstack/ai';
6
+ import { Observable } from 'rxjs';
7
+
8
+ declare const LOCALES: readonly [
9
+ "en",
10
+ "ar"
11
+ ];
12
+ export type Locale = (typeof LOCALES)[number];
13
+ declare const TOOL_SURFACES: readonly [
14
+ "inline",
15
+ "modal"
16
+ ];
17
+ export type ToolSurface = (typeof TOOL_SURFACES)[number];
18
+ declare const ATTACHMENT_STATUSES: readonly [
19
+ "pending",
20
+ "described",
21
+ "failed",
22
+ "skipped"
23
+ ];
24
+ declare const ATTACHMENT_ORIGINS: readonly [
25
+ "upload",
26
+ "tool"
27
+ ];
28
+ export type AttachmentStatus = (typeof ATTACHMENT_STATUSES)[number];
29
+ export type AttachmentOrigin = (typeof ATTACHMENT_ORIGINS)[number];
30
+ export type AttachmentSummary = {
31
+ id: string;
32
+ filename: string;
33
+ contentType: string;
34
+ sizeBytes: number;
35
+ status: AttachmentStatus;
36
+ origin: AttachmentOrigin;
37
+ description: string | null;
38
+ };
39
+ export type ThreadSummary = {
40
+ id: string;
41
+ title: string | null;
42
+ createdAt: string;
43
+ updatedAt: string;
44
+ isRunning: boolean;
45
+ /** Present on a headless thread a trigger owns. Widgets never list these. */
46
+ trigger?: {
47
+ name: string;
48
+ };
49
+ };
50
+ export type TokenUsage = {
51
+ input: {
52
+ noCache: number;
53
+ cacheRead: number;
54
+ cacheWrite: number;
55
+ total: number;
56
+ };
57
+ output: {
58
+ reasoning: number;
59
+ text: number;
60
+ total: number;
61
+ };
62
+ total: number;
63
+ };
64
+ /**
65
+ * How to launch one client tool: stamped by the server, per tool name, on
66
+ * the metadata of an assistant message that may carry its pending call. Kept in
67
+ * the message so the binding survives server restarts while a run is parked.
68
+ */
69
+ export type ClientToolBinding = {
70
+ toolId: string;
71
+ surface: ToolSurface;
72
+ embedOrigin: string;
73
+ /** Variables the initiate call's input reads. */
74
+ variables: string[];
75
+ };
76
+ /**
77
+ * What the server offers this agent's clients, fetched once at widget startup.
78
+ * The widget hides UI for anything absent; enforcement stays server-side.
79
+ */
80
+ export type AgentCapabilities = {
81
+ attachments: boolean;
82
+ /** False when the agent's `message` trigger is off: the widget hides its composer. */
83
+ messages: boolean;
84
+ /** Prompt chips on the blank-chat landing per locale; clicking one sends its `prompt` as the first message. */
85
+ suggestions: Suggestions;
86
+ /** Headline of the blank-chat landing per locale; "" falls back to the widget's built-in copy. */
87
+ greeting: Greeting;
88
+ };
89
+ export type MessageMetadata = {
90
+ /** ISO time the server stored the message; stamped from the row when a transcript is served. */
91
+ createdAt?: string;
92
+ modelId?: string;
93
+ providerMetadata?: unknown;
94
+ isAborted?: boolean;
95
+ tokenUsage?: TokenUsage;
96
+ attachments?: AttachmentSummary[];
97
+ clientTools?: Record<string, ClientToolBinding>;
98
+ /** Set on `trigger` messages: the firing that wrote them. */
99
+ trigger?: TriggerFiringSummary;
100
+ };
101
+ /**
102
+ * One landing-page chip: the widget shows `title` and sends `prompt` as the
103
+ * first message when clicked. The two may be the same text.
104
+ */
105
+ export type Suggestion = {
106
+ title: string;
107
+ prompt: string;
108
+ };
109
+ /** The runtime contract's `suggestionsSchema` and `greetingSchema` are built from these. */
110
+ export type Suggestions = Record<Locale, Suggestion[]>;
111
+ export type Greeting = Record<Locale, string>;
112
+ export type TriggerFiringSummary = {
113
+ name: string;
114
+ firingId: string;
115
+ firedAt: string;
116
+ payload: unknown;
117
+ };
118
+ export type CortexHeaders = Record<string, string>;
119
+ /**
120
+ * How the widget reaches its cortex server. `baseUrl` may be a thunk so a host
121
+ * with a reactive origin (an Angular Signal is itself a `() => string`) is read
122
+ * at the moment of each request rather than captured once.
123
+ */
124
+ export type CortexTransport = {
125
+ baseUrl: string | (() => string);
126
+ getHeaders: () => CortexHeaders | Promise<CortexHeaders>;
127
+ };
128
+ export type ColorScheme = "light" | "dark";
129
+ export type MediaRenderContext = {
130
+ scheme: ColorScheme;
131
+ };
132
+ /**
133
+ * Draws a closed media fence's `source` into `host`, resolves once drawn, and
134
+ * returns the teardown. A consumer's `mediaRenderers` config entry has this
135
+ * shape too: one per fence language, replacing the built-in of the same name
136
+ * or adding a language of its own.
137
+ */
138
+ export type MediaRenderer = (host: HTMLElement, source: string, context: MediaRenderContext) => Promise<() => void>;
139
+ export type MediaRenderers = Partial<Record<string, MediaRenderer>>;
140
+ /** One value for every locale, or a map keyed by locale ('en', 'ar'). */
141
+ export type Localized<T> = T | Partial<Record<Locale, T>>;
142
+ export type CortexViewMode = "helper" | "full";
143
+ export type CortexTheme = "light" | "dark" | "system";
144
+ export type CortexToolCallContext = {
145
+ threadId: string;
146
+ };
147
+ /** A tool the server left for the client to answer. */
148
+ export type CortexToolCall = {
149
+ toolCallId: string;
150
+ toolName: string;
151
+ input: unknown;
152
+ };
153
+ export type CortexHooks = {
154
+ onToolCall?: (toolCall: CortexToolCall, context: CortexToolCallContext) => unknown;
155
+ onThreadSelected?: (thread: ThreadSummary) => Promise<void> | void;
156
+ onThreadDeselected?: (thread: ThreadSummary) => Promise<void> | void;
157
+ };
158
+ /** What the widget hands a consumer-supplied tool component, in either SDK. */
159
+ export type CortexToolComponentInputs = {
160
+ toolCallPart: ToolCallPart;
161
+ message: UIMessage;
162
+ /** Answers the call. Rejects when the delivery failed, so a component can say so and let the user retry. */
163
+ setOutput: (output: unknown) => Promise<void>;
164
+ };
165
+ /**
166
+ * The framework-neutral part of the widget's configuration. Each SDK extends it
167
+ * with its own `toolComponents` slot, which is the one field whose type is
168
+ * inherently framework-bound.
169
+ */
170
+ export type CortexConfig = {
171
+ /** Defaults to `'en'`. */
172
+ locale?: Locale;
173
+ transport: CortexTransport;
174
+ /**
175
+ * WebSocket endpoint override, derived from `transport.baseUrl` by default.
176
+ * `false` disables the socket entirely (chat still works fully over SSE;
177
+ * only cross-tab notify events are lost) — required behind proxies that do
178
+ * not forward `/ws`, which would otherwise reconnect forever.
179
+ */
180
+ wsUrl?: string | false;
181
+ viewMode?: CortexViewMode;
182
+ /** Color scheme; `'system'` follows the OS preference. Defaults to `'light'`. */
183
+ theme?: CortexTheme;
184
+ /**
185
+ * Replaces or extends the media fences the assistant's replies draw. Keyed by
186
+ * fence tag; `map` swaps the built-in Leaflet map for another provider.
187
+ */
188
+ mediaRenderers?: MediaRenderers;
189
+ /**
190
+ * Chat header title while no thread is selected; defaults to a localized
191
+ * "New Chat". Like the other copy props, either one value for every locale
192
+ * or a map keyed by locale (`{ en: 'Support', ar: 'الدعم' }`).
193
+ */
194
+ agentName?: Localized<string>;
195
+ showDebugButton?: boolean;
196
+ /**
197
+ * Render embedded tool pages served from this page's own origin with their
198
+ * full identity — which, same-origin, means no effective sandbox at all: such
199
+ * an embed can reach into the host page. By default they are refused
200
+ * (rendered inert), because embed URLs come from Control Center tool config,
201
+ * and whoever administers that is not necessarily whoever owns this site.
202
+ * Enable only when both are the same party and the embeds are this site's
203
+ * own pages — then a second origin just for embeds is no longer needed.
204
+ */
205
+ trustSameOriginEmbeds?: boolean;
206
+ hooks?: CortexHooks;
207
+ };
208
+ export type QueuedAttachment = {
209
+ localId: string;
210
+ filename: string;
211
+ status: "uploading" | "ready" | "deleting" | "error";
212
+ threadId?: string;
213
+ attachmentId?: string;
214
+ summary?: AttachmentSummary;
215
+ };
216
+ declare const ATTACHMENT_STATUSES$1: readonly [
217
+ "pending",
218
+ "described",
219
+ "failed",
220
+ "skipped"
221
+ ];
222
+ declare const ATTACHMENT_ORIGINS$1: readonly [
223
+ "upload",
224
+ "tool"
225
+ ];
226
+ type AttachmentStatus$1 = (typeof ATTACHMENT_STATUSES$1)[number];
227
+ type AttachmentOrigin$1 = (typeof ATTACHMENT_ORIGINS$1)[number];
228
+ export type AttachmentSummary$1 = {
229
+ id: string;
230
+ filename: string;
231
+ contentType: string;
232
+ sizeBytes: number;
233
+ status: AttachmentStatus$1;
234
+ origin: AttachmentOrigin$1;
235
+ description: string | null;
236
+ };
237
+ export type CortexAngularConfig = CortexConfig & {
238
+ toolComponents?: Record<string, Type<CortexToolComponentInputs>>;
239
+ injector?: Injector;
240
+ };
241
+ declare class AttachmentQueueService {
242
+ readonly items: import("@angular/core").WritableSignal<QueuedAttachment[]>;
243
+ private readonly flags;
244
+ readonly uploading: import("@angular/core").Signal<boolean>;
245
+ readonly busy: import("@angular/core").Signal<boolean>;
246
+ readonly hasReady: import("@angular/core").Signal<boolean>;
247
+ private readonly chatService;
248
+ readonly store: {
249
+ getState: () => QueuedAttachment[];
250
+ subscribe: (listener: () => void) => () => void;
251
+ accept: (files: File[]) => void;
252
+ remove: (localId: string) => void;
253
+ clear: (threadId: string | undefined) => void;
254
+ consumeReady: () => {
255
+ status: "ready";
256
+ summary: AttachmentSummary$1;
257
+ localId: string;
258
+ filename: string;
259
+ threadId?: string;
260
+ attachmentId?: string;
261
+ }[];
262
+ restoreConsumed: (threadId: string | undefined) => void;
263
+ discardConsumed: () => void;
264
+ };
265
+ constructor();
266
+ static ɵfac: i0.ɵɵFactoryDeclaration<AttachmentQueueService, never>;
267
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
268
+ }
269
+ declare class CortexChatService {
270
+ /**
271
+ * Selection is the Thread handed to `selectThread`, resolved against the live
272
+ * list: the row there receives the title and isRunning updates the server
273
+ * pushes, so the selection follows them without hand-syncing. The snapshot
274
+ * only answers for a thread the list has not caught up with yet.
275
+ */
276
+ readonly selectedThread: import("@angular/core").Signal<ThreadSummary | undefined>;
277
+ private readonly chat;
278
+ /** The open conversation's transcript; empty while nothing is selected. */
279
+ readonly messages: import("@angular/core").Signal<import("@tanstack/ai-client").UIMessage<readonly import("@tanstack/ai").AnyClientTool[], any>[]>;
280
+ readonly isLoadingMessages: import("@angular/core").WritableSignal<boolean>;
281
+ readonly isAgentWorking: import("@angular/core").Signal<boolean>;
282
+ /**
283
+ * Derived, never hand-synced: a call only counts as owed while the stream is
284
+ * idle — mid-turn an unsettled call is just a server tool executing. Reading
285
+ * straight from the SDK's signals is what brings the composer back the
286
+ * instant `addToolResult` settles the call part — the SDK's delivery promise
287
+ * only resolves once the continuation turn ends, far too late to gate UI on.
288
+ */
289
+ readonly hasPendingToolCalls: import("@angular/core").Signal<boolean>;
290
+ /**
291
+ * The per-message facts the SDK's `UIMessage` has no room for — usage, model
292
+ * id, the aborted flag, attachments. They are cortex's own envelope around the
293
+ * stored parts, so they arrive with the persisted transcript rather than over
294
+ * the stream, and are merged back in by message id at render time.
295
+ */
296
+ readonly messageMetadata: import("@angular/core").WritableSignal<ReadonlyMap<string, MessageMetadata>>;
297
+ /** What the server offers this agent's clients; undefined until the handshake answers. */
298
+ readonly capabilities: import("@angular/core").WritableSignal<AgentCapabilities | undefined>;
299
+ private readonly events;
300
+ private readonly selectedThreadSnapshot;
301
+ private readonly config;
302
+ private readonly api;
303
+ private readonly threadService;
304
+ private readonly wsService;
305
+ private readonly destroyRef;
306
+ private readonly environmentInjector;
307
+ /** Owns the current thread's chat client; destroying it stops and disposes it. */
308
+ private chatInjector;
309
+ /** The turn in flight on the current chat client; replaced with it on every mount. */
310
+ private lifecycle;
311
+ private pendingThreadCreation;
312
+ private refreshing;
313
+ private stopCapabilities;
314
+ constructor();
315
+ /**
316
+ * Kicked off by the widget once its `config` input is bound. This cannot happen in
317
+ * the constructor: the widget builds its children during its creation pass, which
318
+ * Angular runs before it binds inputs, so the transport is not knowable yet.
319
+ */
320
+ start(): void;
321
+ /**
322
+ * Capability-gated controls stay hidden until the handshake answers, so it
323
+ * is asked until it does, and afresh after every reconnect: the old answer
324
+ * may be stale (a trigger toggled while the socket was down), so it is
325
+ * forgotten first.
326
+ */
327
+ private loadCapabilities;
328
+ get events$(): Observable<"onFinish" | "onError" | "onThreadSelected" | "onSend">;
329
+ selectThread(thread: ThreadSummary, options?: {
330
+ skipLoadingMessages?: boolean;
331
+ }): Observable<ThreadSummary>;
332
+ deselectThread(): void;
333
+ /**
334
+ * The id of the thread the next message belongs to, creating it when the widget is
335
+ * still on a blank chat.
336
+ */
337
+ ensureThread(): Observable<string>;
338
+ send(prompt: string, attachments?: AttachmentSummary[]): Observable<void>;
339
+ /** Answers a tool call on behalf of a consumer-supplied tool component.
340
+ * Returns the delivery promise so callers (the client tool embed) can
341
+ * re-arm on a transient failure instead of stranding the pending call. */
342
+ addToolResult(toolCallId: string, toolName: string, output: unknown): Promise<void>;
343
+ /** The user's decision on a server tool the run parked on; the run resumes once delivered. */
344
+ respondToApproval(approvalId: string, approved: boolean): Promise<void>;
345
+ abort(): Promise<void>;
346
+ deleteThread(threadId: string): Observable<void>;
347
+ /**
348
+ * Points the widget at a thread with a chat client of its own. The client is
349
+ * server-authoritative: on mount it asks cortex for the transcript and for
350
+ * whatever run is still generating, so selecting a thread, reloading the page
351
+ * and a second tab all re-attach through one path.
352
+ */
353
+ private mountChat;
354
+ /**
355
+ * Re-mounts the current thread's chat so its hydration re-reads the transcript
356
+ * and re-attaches to whatever run is live now. This is how a run another tab
357
+ * started, and a websocket that dropped and came back, land in this widget.
358
+ * A caller passes the thread when it holds fresher state than the list.
359
+ */
360
+ private reattach;
361
+ private createConnection;
362
+ private refreshMessages;
363
+ private readBackMessages;
364
+ /**
365
+ * Server rows are merged over what is held, never swapped in: the entry this
366
+ * client seeded for a message it just sent must outlive a hydration or
367
+ * read-back that started before the send.
368
+ */
369
+ private absorbMetadata;
370
+ private setMetadata;
371
+ private subscribeToWsEvents;
372
+ /**
373
+ * Only what these events mean for the open conversation — their list-shaped
374
+ * side is CortexThreadService's, which absorbs them on its own subscription.
375
+ */
376
+ private onWsEvent;
377
+ private subscribeToWsConnections;
378
+ static ɵfac: i0.ɵɵFactoryDeclaration<CortexChatService, never>;
379
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
380
+ }
381
+ export declare class CortexChatWidgetComponent implements OnInit {
382
+ readonly config: import("@angular/core").InputSignal<CortexAngularConfig>;
383
+ protected readonly debugMode: import("@angular/core").WritableSignal<boolean>;
384
+ protected readonly screen: import("@angular/core").WritableSignal<"threads" | "chat">;
385
+ protected readonly sidebarOpen: import("@angular/core").WritableSignal<boolean>;
386
+ protected readonly viewMode: import("@angular/core").Signal<CortexViewMode>;
387
+ private readonly composer;
388
+ protected chatService: CortexChatService;
389
+ protected queue: AttachmentQueueService;
390
+ private readonly injector;
391
+ private readonly destroyRef;
392
+ ngOnInit(): void;
393
+ protected toggleSidebar(): void;
394
+ protected closeSidebar(): void;
395
+ protected toggleDebugMode(): void;
396
+ protected selectThread(thread: ThreadSummary): void;
397
+ protected openHistory(): void;
398
+ protected closeHistory(): void;
399
+ protected onWidgetDragOver(event: DragEvent): void;
400
+ protected onWidgetDrop(event: DragEvent): void;
401
+ protected newChat(): void;
402
+ static ɵfac: i0.ɵɵFactoryDeclaration<CortexChatWidgetComponent, never>;
403
+ static ɵcmp: i0.ɵɵComponentDeclaration<CortexChatWidgetComponent, "cortex-chat-widget", never, {
404
+ "config": {
405
+ "alias": "config";
406
+ "required": true;
407
+ "isSignal": true;
408
+ };
409
+ }, {}, never, never, true, never>;
410
+ }
411
+
412
+ export {};