lumiverse-spindle-types 0.5.1 → 0.5.3
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 +1 -1
- package/src/api.ts +104 -1
- package/src/components.ts +496 -0
- package/src/dom.ts +9 -0
- package/src/index.ts +47 -0
- package/src/permissions.ts +6 -1
- package/src/spindle-api.ts +71 -0
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -1266,6 +1266,99 @@ export interface PermissionChangedDetail {
|
|
|
1266
1266
|
allGranted: string[];
|
|
1267
1267
|
}
|
|
1268
1268
|
|
|
1269
|
+
// ─── Web Search DTOs ────────────────────────────────────────────────────
|
|
1270
|
+
|
|
1271
|
+
/** Identifies which provider backs the user's configured web search engine. */
|
|
1272
|
+
export type WebSearchProviderDTO = "searxng";
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Safe view of a user's web search configuration. The raw API key is never
|
|
1276
|
+
* exposed — only `hasApiKey` indicates whether one is on file.
|
|
1277
|
+
*/
|
|
1278
|
+
export interface WebSearchSettingsDTO {
|
|
1279
|
+
enabled: boolean;
|
|
1280
|
+
provider: WebSearchProviderDTO;
|
|
1281
|
+
apiUrl: string;
|
|
1282
|
+
requestTimeoutMs: number;
|
|
1283
|
+
defaultResultCount: number;
|
|
1284
|
+
maxResultCount: number;
|
|
1285
|
+
maxPagesToScrape: number;
|
|
1286
|
+
maxCharsPerPage: number;
|
|
1287
|
+
language: string;
|
|
1288
|
+
safeSearch: 0 | 1 | 2;
|
|
1289
|
+
engines: string[];
|
|
1290
|
+
hasApiKey: boolean;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/** A single search result row, normalized across providers. */
|
|
1294
|
+
export interface WebSearchResultDTO {
|
|
1295
|
+
title: string;
|
|
1296
|
+
url: string;
|
|
1297
|
+
snippet: string;
|
|
1298
|
+
/** Provider-reported engine identifier when available (e.g. `"google"`, `"bing"`). */
|
|
1299
|
+
engine?: string;
|
|
1300
|
+
/** Provider-reported relevance score when available. */
|
|
1301
|
+
score?: number;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* A search result enriched with scraped page content. Only present when the
|
|
1306
|
+
* query was run with `scrape: true` (the default).
|
|
1307
|
+
*/
|
|
1308
|
+
export interface WebSearchDocumentDTO {
|
|
1309
|
+
title: string;
|
|
1310
|
+
url: string;
|
|
1311
|
+
snippet: string;
|
|
1312
|
+
/** How the page content was extracted (e.g. `"html"`, `"pdf"`). */
|
|
1313
|
+
sourceType?: string;
|
|
1314
|
+
/** Extracted page text, clipped to `WebSearchSettingsDTO.maxCharsPerPage`. */
|
|
1315
|
+
content?: string;
|
|
1316
|
+
/** Length of the source page content before clipping. */
|
|
1317
|
+
contentLength?: number;
|
|
1318
|
+
/** Populated when scraping failed for this result; `content` is then absent. */
|
|
1319
|
+
error?: string;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/** Options forwarded to `spindle.webSearch.query()`. */
|
|
1323
|
+
export interface WebSearchRequestDTO {
|
|
1324
|
+
/** Free-text search query. Trimmed by the host; empty values are rejected. */
|
|
1325
|
+
query: string;
|
|
1326
|
+
/**
|
|
1327
|
+
* Desired number of results. Clamped to `WebSearchSettingsDTO.maxResultCount`
|
|
1328
|
+
* on the host; omit to use the user's `defaultResultCount`.
|
|
1329
|
+
*/
|
|
1330
|
+
count?: number;
|
|
1331
|
+
/**
|
|
1332
|
+
* When `true` (default), the host scrapes the first
|
|
1333
|
+
* `WebSearchSettingsDTO.maxPagesToScrape` results, fills in
|
|
1334
|
+
* `documents[].content`, and assembles the `context` block. Set to `false`
|
|
1335
|
+
* to skip scraping entirely — only `results` are returned, and
|
|
1336
|
+
* `documents` / `context` are omitted from the response. Useful when the
|
|
1337
|
+
* extension only needs titles, URLs, and snippets.
|
|
1338
|
+
*/
|
|
1339
|
+
scrape?: boolean;
|
|
1340
|
+
/** For operator-scoped extensions; ignored on user-scoped extensions. */
|
|
1341
|
+
userId?: string;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Result of a successful `spindle.webSearch.query()` call. `documents` and
|
|
1346
|
+
* `context` are omitted when the request was issued with `scrape: false`.
|
|
1347
|
+
*/
|
|
1348
|
+
export interface WebSearchResponseDTO {
|
|
1349
|
+
/** The (trimmed) query that was executed. */
|
|
1350
|
+
query: string;
|
|
1351
|
+
/** Raw normalized results from the search provider. */
|
|
1352
|
+
results: WebSearchResultDTO[];
|
|
1353
|
+
/** Per-result scraped page content. Absent when `scrape: false`. */
|
|
1354
|
+
documents?: WebSearchDocumentDTO[];
|
|
1355
|
+
/**
|
|
1356
|
+
* Pre-assembled, prompt-ready context block summarizing the query plus the
|
|
1357
|
+
* scraped documents. Absent when `scrape: false`.
|
|
1358
|
+
*/
|
|
1359
|
+
context?: string;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1269
1362
|
// ─── Theme DTOs ──────────────────────────────────────────────────────────
|
|
1270
1363
|
|
|
1271
1364
|
/**
|
|
@@ -2448,7 +2541,17 @@ export type WorkerToHost =
|
|
|
2448
2541
|
model?: string;
|
|
2449
2542
|
modelSource?: TokenModelSourceDTO;
|
|
2450
2543
|
userId?: string;
|
|
2451
|
-
}
|
|
2544
|
+
}
|
|
2545
|
+
// ─── Web Search (gated: "web_search") ─────────────────────────────────
|
|
2546
|
+
| {
|
|
2547
|
+
type: "web_search_query";
|
|
2548
|
+
requestId: string;
|
|
2549
|
+
query: string;
|
|
2550
|
+
count?: number;
|
|
2551
|
+
scrape?: boolean;
|
|
2552
|
+
userId?: string;
|
|
2553
|
+
}
|
|
2554
|
+
| { type: "web_search_get_settings"; requestId: string; userId?: string };
|
|
2452
2555
|
|
|
2453
2556
|
// ─── Host → Worker messages ──────────────────────────────────────────────
|
|
2454
2557
|
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-provided shared UI components.
|
|
3
|
+
*
|
|
4
|
+
* These types describe an API surface that lets a frontend extension mount
|
|
5
|
+
* instances of Lumiverse's first-party React components inside extension-owned
|
|
6
|
+
* DOM nodes (drawer tabs, float widgets, dock panels, app mounts, message
|
|
7
|
+
* widgets, etc.). The host renders the real React component into the supplied
|
|
8
|
+
* target element — extensions never need to depend on React themselves.
|
|
9
|
+
*
|
|
10
|
+
* Common shape:
|
|
11
|
+
* - Each `mountX(target, options)` call returns a `SpindleMountedComponent`
|
|
12
|
+
* handle scoped to the supplied target.
|
|
13
|
+
* - Options carry initial values plus user-event callbacks (e.g. `onChange`).
|
|
14
|
+
* - The host owns internal state and re-renders on user interaction. To
|
|
15
|
+
* programmatically change inputs from the extension side, call
|
|
16
|
+
* `handle.update({ ... })`. To read current state, call `handle.getValue()`
|
|
17
|
+
* where available.
|
|
18
|
+
* - Calling `handle.destroy()` unmounts the React tree and removes any host
|
|
19
|
+
* resources. The supplied target element is not removed from the DOM —
|
|
20
|
+
* extensions own placement and lifetime of the container.
|
|
21
|
+
*
|
|
22
|
+
* Components inherit the active Lumiverse theme via CSS variables, so they
|
|
23
|
+
* visually match the rest of the host UI without any additional wiring.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Shared base shapes
|
|
28
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Common handle shape returned by every `ctx.components.mount*` call.
|
|
32
|
+
*
|
|
33
|
+
* @template TOptions Options shape used at mount time. `update()` accepts a
|
|
34
|
+
* partial of the same shape so callers get IntelliSense for every field they
|
|
35
|
+
* are allowed to change post-mount.
|
|
36
|
+
*/
|
|
37
|
+
export interface SpindleMountedComponent<TOptions> {
|
|
38
|
+
/** Host-assigned component instance ID, unique per extension. */
|
|
39
|
+
readonly componentId: string;
|
|
40
|
+
/** The container element the component was mounted into. Same node passed as `target`. */
|
|
41
|
+
readonly element: HTMLElement;
|
|
42
|
+
/**
|
|
43
|
+
* Merge a partial set of options into the live component. Pass any subset of
|
|
44
|
+
* the original mount options — undefined fields are ignored.
|
|
45
|
+
*/
|
|
46
|
+
update(patch: Partial<TOptions>): void;
|
|
47
|
+
/** Unmount the React tree and release host resources. The target element is left in place. */
|
|
48
|
+
destroy(): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Mount-time target. Either a CSS selector resolved against the extension-owned DOM, or an Element. */
|
|
52
|
+
export type SpindleComponentTarget = string | Element;
|
|
53
|
+
|
|
54
|
+
/** Connection kinds the host can wire shared pickers up to. */
|
|
55
|
+
export type SpindleConnectionKind = "llm" | "image" | "tts" | "embedding";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reference to a host-managed connection profile.
|
|
59
|
+
*
|
|
60
|
+
* When supplied to a connection-aware picker (e.g. {@link SpindleModelComboboxOptions}),
|
|
61
|
+
* the host populates available choices, loading state, and refresh behavior
|
|
62
|
+
* from its own connection registry. The extension does not need to fetch or
|
|
63
|
+
* cache anything itself.
|
|
64
|
+
*/
|
|
65
|
+
export interface SpindleConnectionRef {
|
|
66
|
+
/** Which connection family the picker should bind to. */
|
|
67
|
+
kind: SpindleConnectionKind;
|
|
68
|
+
/**
|
|
69
|
+
* Specific connection ID. If omitted, the host binds to the currently
|
|
70
|
+
* active connection of the given `kind` and follows live changes when the
|
|
71
|
+
* user switches active connections.
|
|
72
|
+
*/
|
|
73
|
+
id?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// Text inputs
|
|
78
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export interface SpindleTextInputOptions {
|
|
81
|
+
/** Initial value. */
|
|
82
|
+
value?: string;
|
|
83
|
+
/** Fired whenever the user changes the input. */
|
|
84
|
+
onChange?: (value: string) => void;
|
|
85
|
+
/** Placeholder text shown when value is empty. */
|
|
86
|
+
placeholder?: string;
|
|
87
|
+
/** Focus the input on mount. */
|
|
88
|
+
autoFocus?: boolean;
|
|
89
|
+
/** Disable user interaction. */
|
|
90
|
+
disabled?: boolean;
|
|
91
|
+
/** Additional CSS class merged onto the input element. */
|
|
92
|
+
className?: string;
|
|
93
|
+
/** Optional accessible label. Surfaces as `aria-label` on the input. */
|
|
94
|
+
ariaLabel?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SpindleTextInputHandle extends SpindleMountedComponent<SpindleTextInputOptions> {
|
|
98
|
+
/** Read the current value. */
|
|
99
|
+
getValue(): string;
|
|
100
|
+
/** Programmatically focus the input. */
|
|
101
|
+
focus(): void;
|
|
102
|
+
/** Programmatically blur the input. */
|
|
103
|
+
blur(): void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface SpindleTextAreaOptions {
|
|
107
|
+
value?: string;
|
|
108
|
+
onChange?: (value: string) => void;
|
|
109
|
+
placeholder?: string;
|
|
110
|
+
/** Visible row count. Default: `4`. */
|
|
111
|
+
rows?: number;
|
|
112
|
+
disabled?: boolean;
|
|
113
|
+
className?: string;
|
|
114
|
+
ariaLabel?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface SpindleTextAreaHandle extends SpindleMountedComponent<SpindleTextAreaOptions> {
|
|
118
|
+
getValue(): string;
|
|
119
|
+
focus(): void;
|
|
120
|
+
blur(): void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
124
|
+
// Numeric inputs
|
|
125
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
export interface SpindleNumericInputOptions {
|
|
128
|
+
/** Initial value, or `null` for empty. */
|
|
129
|
+
value?: number | null;
|
|
130
|
+
/** Fired whenever the value changes. `null` means the field is empty. */
|
|
131
|
+
onChange?: (value: number | null) => void;
|
|
132
|
+
/** Allow the field to be empty (`null`). Default: `false`. */
|
|
133
|
+
allowEmpty?: boolean;
|
|
134
|
+
/** Restrict input to integers. Default: `false`. */
|
|
135
|
+
integer?: boolean;
|
|
136
|
+
/** Minimum allowed value. */
|
|
137
|
+
min?: number;
|
|
138
|
+
/** Maximum allowed value. */
|
|
139
|
+
max?: number;
|
|
140
|
+
/** Step size used by the underlying `<input type="number">`. */
|
|
141
|
+
step?: number;
|
|
142
|
+
placeholder?: string;
|
|
143
|
+
disabled?: boolean;
|
|
144
|
+
className?: string;
|
|
145
|
+
ariaLabel?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface SpindleNumericInputHandle extends SpindleMountedComponent<SpindleNumericInputOptions> {
|
|
149
|
+
getValue(): number | null;
|
|
150
|
+
focus(): void;
|
|
151
|
+
blur(): void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface SpindleNumberStepperOptions {
|
|
155
|
+
value?: number | null;
|
|
156
|
+
onChange?: (value: number | null) => void;
|
|
157
|
+
min?: number;
|
|
158
|
+
max?: number;
|
|
159
|
+
/** Increment/decrement step. Default: `1`. */
|
|
160
|
+
step?: number;
|
|
161
|
+
allowEmpty?: boolean;
|
|
162
|
+
integer?: boolean;
|
|
163
|
+
placeholder?: string;
|
|
164
|
+
disabled?: boolean;
|
|
165
|
+
className?: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface SpindleNumberStepperHandle extends SpindleMountedComponent<SpindleNumberStepperOptions> {
|
|
169
|
+
getValue(): number | null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
173
|
+
// Boolean inputs
|
|
174
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
export interface SpindleCheckboxOptions {
|
|
177
|
+
checked?: boolean;
|
|
178
|
+
onChange?: (checked: boolean) => void;
|
|
179
|
+
/** Label rendered next to the checkbox. */
|
|
180
|
+
label?: string;
|
|
181
|
+
/** Optional helper text rendered below the label. */
|
|
182
|
+
hint?: string;
|
|
183
|
+
disabled?: boolean;
|
|
184
|
+
className?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface SpindleCheckboxHandle extends SpindleMountedComponent<SpindleCheckboxOptions> {
|
|
188
|
+
getValue(): boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface SpindleSwitchOptions {
|
|
192
|
+
checked?: boolean;
|
|
193
|
+
onChange?: (checked: boolean) => void;
|
|
194
|
+
/** Visual size. Default: `"md"`. */
|
|
195
|
+
size?: "sm" | "md";
|
|
196
|
+
disabled?: boolean;
|
|
197
|
+
className?: string;
|
|
198
|
+
ariaLabel?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface SpindleSwitchHandle extends SpindleMountedComponent<SpindleSwitchOptions> {
|
|
202
|
+
getValue(): boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
206
|
+
// Pickers & selects
|
|
207
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
/** A single option in a select-style picker. */
|
|
210
|
+
export interface SpindleSelectOption {
|
|
211
|
+
/** Stable value emitted to `onChange`. */
|
|
212
|
+
value: string;
|
|
213
|
+
/** Display label. */
|
|
214
|
+
label: string;
|
|
215
|
+
/** Secondary text rendered beneath the label. */
|
|
216
|
+
sublabel?: string;
|
|
217
|
+
/** Group key. Options sharing a group are visually clustered with a header. */
|
|
218
|
+
group?: string;
|
|
219
|
+
/** Render as disabled. */
|
|
220
|
+
disabled?: boolean;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface SpindleSelectOptionsBase {
|
|
224
|
+
/** Available options. */
|
|
225
|
+
options?: SpindleSelectOption[];
|
|
226
|
+
/** Placeholder text shown when no value is selected. */
|
|
227
|
+
placeholder?: string;
|
|
228
|
+
/** Placeholder for the search field inside the dropdown. */
|
|
229
|
+
searchPlaceholder?: string;
|
|
230
|
+
/** Minimum option count before the search field is shown. Default: `8`. */
|
|
231
|
+
searchThreshold?: number;
|
|
232
|
+
/** Message rendered when the filtered option list is empty. */
|
|
233
|
+
emptyMessage?: string;
|
|
234
|
+
/**
|
|
235
|
+
* Render the dropdown into a React portal anchored to `document.body` so it
|
|
236
|
+
* escapes containers with `overflow:hidden`. Default: `true`.
|
|
237
|
+
*/
|
|
238
|
+
portal?: boolean;
|
|
239
|
+
/** Dropdown horizontal alignment relative to the trigger. Default: `"left"`. */
|
|
240
|
+
align?: "left" | "right";
|
|
241
|
+
/** Maximum dropdown height in CSS pixels. */
|
|
242
|
+
maxHeight?: number;
|
|
243
|
+
/** Minimum dropdown width in CSS pixels. */
|
|
244
|
+
minWidth?: number;
|
|
245
|
+
disabled?: boolean;
|
|
246
|
+
className?: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface SpindleSelectOptions extends SpindleSelectOptionsBase {
|
|
250
|
+
/** Initial value. */
|
|
251
|
+
value?: string;
|
|
252
|
+
onChange?: (value: string) => void;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface SpindleSelectHandle extends SpindleMountedComponent<SpindleSelectOptions> {
|
|
256
|
+
getValue(): string;
|
|
257
|
+
/** Open the dropdown programmatically. */
|
|
258
|
+
open(): void;
|
|
259
|
+
/** Close the dropdown programmatically. */
|
|
260
|
+
close(): void;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface SpindleMultiSelectOptions extends SpindleSelectOptionsBase {
|
|
264
|
+
/** Initial value. */
|
|
265
|
+
value?: string[];
|
|
266
|
+
onChange?: (value: string[]) => void;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface SpindleMultiSelectHandle extends SpindleMountedComponent<SpindleMultiSelectOptions> {
|
|
270
|
+
getValue(): string[];
|
|
271
|
+
open(): void;
|
|
272
|
+
close(): void;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Connection-aware model picker.
|
|
277
|
+
*
|
|
278
|
+
* **Recommended (connection-bound) mode:** supply `connection` and the host
|
|
279
|
+
* will populate `models`, drive the refresh button, manage loading state, and
|
|
280
|
+
* react to live connection changes.
|
|
281
|
+
*
|
|
282
|
+
* **Manual mode:** supply `models`, `loading`, and `onRefresh` directly. Use
|
|
283
|
+
* this when the extension fetches model lists from its own backend or proxy.
|
|
284
|
+
*
|
|
285
|
+
* The two modes are mutually exclusive. If both `connection` and `models` are
|
|
286
|
+
* supplied, `connection` wins and the manual fields are ignored.
|
|
287
|
+
*/
|
|
288
|
+
export interface SpindleModelComboboxOptions {
|
|
289
|
+
/** Currently entered model ID. */
|
|
290
|
+
value?: string;
|
|
291
|
+
/** Fired when the user types or selects a model. */
|
|
292
|
+
onChange?: (value: string) => void;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Bind to a host-managed connection. The host populates the model list,
|
|
296
|
+
* handles the refresh affordance, and reports loading state automatically.
|
|
297
|
+
*
|
|
298
|
+
* Pass just `{ kind }` to follow the user's currently active connection of
|
|
299
|
+
* that kind. Pass `{ kind, id }` to pin to a specific connection.
|
|
300
|
+
*/
|
|
301
|
+
connection?: SpindleConnectionRef;
|
|
302
|
+
|
|
303
|
+
/** Manual mode: explicit model list. Ignored when `connection` is set. */
|
|
304
|
+
models?: string[];
|
|
305
|
+
/** Manual mode: optional map of model ID → human-readable label. */
|
|
306
|
+
modelLabels?: Record<string, string>;
|
|
307
|
+
/** Manual mode: surface the host spinner inside the refresh affordance. */
|
|
308
|
+
loading?: boolean;
|
|
309
|
+
/** Manual mode: render a refresh button that invokes this handler when clicked. */
|
|
310
|
+
onRefresh?: () => void;
|
|
311
|
+
|
|
312
|
+
/** Trigger an auto-refresh the first time the input gains focus (manual mode only). */
|
|
313
|
+
autoRefreshOnFocus?: boolean;
|
|
314
|
+
/** Opaque key — when it changes, the host re-arms the autoRefreshOnFocus guard. */
|
|
315
|
+
refreshKey?: string;
|
|
316
|
+
|
|
317
|
+
/** Visual density. `"compact"` matches inline rows; `"standard"` matches form rows; `"editor"` matches the prompt editor. */
|
|
318
|
+
appearance?: "compact" | "standard" | "editor";
|
|
319
|
+
placeholder?: string;
|
|
320
|
+
emptyMessage?: string;
|
|
321
|
+
loadingMessage?: string;
|
|
322
|
+
/** Optional hint shown beneath the input (e.g. "Search the catalog"). */
|
|
323
|
+
browseHint?: string;
|
|
324
|
+
disabled?: boolean;
|
|
325
|
+
className?: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface SpindleModelComboboxHandle extends SpindleMountedComponent<SpindleModelComboboxOptions> {
|
|
329
|
+
getValue(): string;
|
|
330
|
+
/** Force a refresh of the host-managed model list. No-op in manual mode unless `onRefresh` is set. */
|
|
331
|
+
refresh(): void;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface SpindleFolderDropdownOptions {
|
|
335
|
+
/** Available folder names. */
|
|
336
|
+
folders?: string[];
|
|
337
|
+
/** Currently selected folder. */
|
|
338
|
+
value?: string;
|
|
339
|
+
onChange?: (folder: string) => void;
|
|
340
|
+
/** Fired when the user creates a new folder via the inline affordance. */
|
|
341
|
+
onCreateFolder?: (name: string) => void;
|
|
342
|
+
placeholder?: string;
|
|
343
|
+
disabled?: boolean;
|
|
344
|
+
className?: string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface SpindleFolderDropdownHandle extends SpindleMountedComponent<SpindleFolderDropdownOptions> {
|
|
348
|
+
getValue(): string;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
352
|
+
// Display & layout
|
|
353
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
export type SpindleBadgeColor = "neutral" | "primary" | "success" | "warning" | "danger" | "info";
|
|
356
|
+
|
|
357
|
+
export interface SpindleBadgeOptions {
|
|
358
|
+
/** Badge text. */
|
|
359
|
+
text?: string;
|
|
360
|
+
/** Accent color. Default: `"neutral"`. */
|
|
361
|
+
color?: SpindleBadgeColor;
|
|
362
|
+
/** Visual size. Default: `"md"`. */
|
|
363
|
+
size?: "sm" | "md" | "pill";
|
|
364
|
+
className?: string;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export type SpindleBadgeHandle = SpindleMountedComponent<SpindleBadgeOptions>;
|
|
368
|
+
|
|
369
|
+
export interface SpindleSpinnerOptions {
|
|
370
|
+
/** Diameter in CSS pixels. Default: `16`. */
|
|
371
|
+
size?: number;
|
|
372
|
+
/** Use the faster rotation variant for impatient contexts. */
|
|
373
|
+
fast?: boolean;
|
|
374
|
+
className?: string;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export type SpindleSpinnerHandle = SpindleMountedComponent<SpindleSpinnerOptions>;
|
|
378
|
+
|
|
379
|
+
export interface SpindleCollapsibleSectionOptions {
|
|
380
|
+
/** Section title rendered in the collapsible header. */
|
|
381
|
+
title: string;
|
|
382
|
+
/** Inline SVG string for an optional leading icon. */
|
|
383
|
+
iconSvg?: string;
|
|
384
|
+
/** URL to an icon image. Mutually exclusive with `iconSvg`. */
|
|
385
|
+
iconUrl?: string;
|
|
386
|
+
/** Optional badge text shown next to the title. */
|
|
387
|
+
badge?: string | number;
|
|
388
|
+
/** Initial expanded state. Default: `true`. */
|
|
389
|
+
defaultExpanded?: boolean;
|
|
390
|
+
/** Fired whenever the user toggles the section. */
|
|
391
|
+
onToggle?: (expanded: boolean) => void;
|
|
392
|
+
className?: string;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export interface SpindleCollapsibleSectionHandle extends SpindleMountedComponent<SpindleCollapsibleSectionOptions> {
|
|
396
|
+
/**
|
|
397
|
+
* Body container the extension owns. Append child elements here — the host
|
|
398
|
+
* renders the collapsible chrome around them.
|
|
399
|
+
*/
|
|
400
|
+
readonly body: HTMLElement;
|
|
401
|
+
/** Returns the current expanded state. */
|
|
402
|
+
isExpanded(): boolean;
|
|
403
|
+
/** Expand the section. */
|
|
404
|
+
expand(): void;
|
|
405
|
+
/** Collapse the section. */
|
|
406
|
+
collapse(): void;
|
|
407
|
+
/** Toggle the section. */
|
|
408
|
+
toggle(): void;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface SpindlePaginationOptions {
|
|
412
|
+
currentPage: number;
|
|
413
|
+
totalPages: number;
|
|
414
|
+
onPageChange: (page: number) => void;
|
|
415
|
+
/** Optional items-per-page selector. Omit to hide the selector entirely. */
|
|
416
|
+
perPage?: number;
|
|
417
|
+
perPageOptions?: number[];
|
|
418
|
+
onPerPageChange?: (perPage: number) => void;
|
|
419
|
+
/** Total item count for the "Showing X–Y of N" summary. Omit to hide the summary. */
|
|
420
|
+
totalItems?: number;
|
|
421
|
+
className?: string;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export type SpindlePaginationHandle = SpindleMountedComponent<SpindlePaginationOptions>;
|
|
425
|
+
|
|
426
|
+
export interface SpindleCloseButtonOptions {
|
|
427
|
+
onClick?: () => void;
|
|
428
|
+
/** Visual size. Default: `"md"`. */
|
|
429
|
+
size?: "sm" | "md";
|
|
430
|
+
/** Visual variant. Default: `"subtle"`. */
|
|
431
|
+
variant?: "subtle" | "solid";
|
|
432
|
+
/** Positioning behavior. Default: `"static"`. */
|
|
433
|
+
position?: "static" | "absolute";
|
|
434
|
+
/** Icon size override in CSS pixels. */
|
|
435
|
+
iconSize?: number;
|
|
436
|
+
ariaLabel?: string;
|
|
437
|
+
className?: string;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export type SpindleCloseButtonHandle = SpindleMountedComponent<SpindleCloseButtonOptions>;
|
|
441
|
+
|
|
442
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
443
|
+
// Aggregator
|
|
444
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Mount points for host-rendered shared components.
|
|
448
|
+
*
|
|
449
|
+
* Each method takes a `target` (the DOM node or selector inside the
|
|
450
|
+
* extension-owned tree to render into) and an `options` object. The returned
|
|
451
|
+
* handle exposes `update()`, `destroy()`, and component-specific helpers.
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```ts
|
|
455
|
+
* // Inside a drawer tab
|
|
456
|
+
* const tab = ctx.ui.registerDrawerTab({ id: "settings", title: "My Tab" })
|
|
457
|
+
* const wrap = ctx.dom.createElement("div")
|
|
458
|
+
* tab.root.appendChild(wrap)
|
|
459
|
+
*
|
|
460
|
+
* const picker = ctx.components.mountModelCombobox(wrap, {
|
|
461
|
+
* value: "",
|
|
462
|
+
* connection: { kind: "llm" },
|
|
463
|
+
* appearance: "standard",
|
|
464
|
+
* onChange: (model) => ctx.sendToBackend({ type: "set_model", model }),
|
|
465
|
+
* })
|
|
466
|
+
*
|
|
467
|
+
* // Later — force the input to a specific value
|
|
468
|
+
* picker.update({ value: "claude-opus-4-7" })
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
export interface SpindleComponentsHelper {
|
|
472
|
+
// Text inputs
|
|
473
|
+
mountTextInput(target: SpindleComponentTarget, options?: SpindleTextInputOptions): SpindleTextInputHandle;
|
|
474
|
+
mountTextArea(target: SpindleComponentTarget, options?: SpindleTextAreaOptions): SpindleTextAreaHandle;
|
|
475
|
+
|
|
476
|
+
// Numeric inputs
|
|
477
|
+
mountNumericInput(target: SpindleComponentTarget, options?: SpindleNumericInputOptions): SpindleNumericInputHandle;
|
|
478
|
+
mountNumberStepper(target: SpindleComponentTarget, options?: SpindleNumberStepperOptions): SpindleNumberStepperHandle;
|
|
479
|
+
|
|
480
|
+
// Boolean inputs
|
|
481
|
+
mountCheckbox(target: SpindleComponentTarget, options?: SpindleCheckboxOptions): SpindleCheckboxHandle;
|
|
482
|
+
mountSwitch(target: SpindleComponentTarget, options?: SpindleSwitchOptions): SpindleSwitchHandle;
|
|
483
|
+
|
|
484
|
+
// Pickers & selects
|
|
485
|
+
mountSelect(target: SpindleComponentTarget, options: SpindleSelectOptions): SpindleSelectHandle;
|
|
486
|
+
mountMultiSelect(target: SpindleComponentTarget, options: SpindleMultiSelectOptions): SpindleMultiSelectHandle;
|
|
487
|
+
mountModelCombobox(target: SpindleComponentTarget, options: SpindleModelComboboxOptions): SpindleModelComboboxHandle;
|
|
488
|
+
mountFolderDropdown(target: SpindleComponentTarget, options: SpindleFolderDropdownOptions): SpindleFolderDropdownHandle;
|
|
489
|
+
|
|
490
|
+
// Display & layout
|
|
491
|
+
mountBadge(target: SpindleComponentTarget, options: SpindleBadgeOptions): SpindleBadgeHandle;
|
|
492
|
+
mountSpinner(target: SpindleComponentTarget, options?: SpindleSpinnerOptions): SpindleSpinnerHandle;
|
|
493
|
+
mountCollapsibleSection(target: SpindleComponentTarget, options: SpindleCollapsibleSectionOptions): SpindleCollapsibleSectionHandle;
|
|
494
|
+
mountPagination(target: SpindleComponentTarget, options: SpindlePaginationOptions): SpindlePaginationHandle;
|
|
495
|
+
mountCloseButton(target: SpindleComponentTarget, options?: SpindleCloseButtonOptions): SpindleCloseButtonHandle;
|
|
496
|
+
}
|
package/src/dom.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RequestInitDTO } from "./api";
|
|
2
|
+
import type { SpindleComponentsHelper } from "./components";
|
|
2
3
|
|
|
3
4
|
/** DOM helper API provided to frontend extension modules. */
|
|
4
5
|
export interface SpindleDOMHelper {
|
|
@@ -598,6 +599,14 @@ export interface SpindleFrontendContext {
|
|
|
598
599
|
*/
|
|
599
600
|
showConfirm(options: SpindleConfirmOptions): Promise<SpindleConfirmResult>;
|
|
600
601
|
};
|
|
602
|
+
/**
|
|
603
|
+
* Mount instances of Lumiverse's first-party shared UI components (form
|
|
604
|
+
* atoms, model picker, select, pagination, etc.) inside extension-owned DOM.
|
|
605
|
+
* The host renders real React components into the supplied container — the
|
|
606
|
+
* extension never needs to depend on React directly. See
|
|
607
|
+
* {@link SpindleComponentsHelper} for the full surface.
|
|
608
|
+
*/
|
|
609
|
+
components: SpindleComponentsHelper;
|
|
601
610
|
uploads: {
|
|
602
611
|
pickFile(options?: {
|
|
603
612
|
accept?: string[];
|
package/src/index.ts
CHANGED
|
@@ -155,6 +155,12 @@ export type {
|
|
|
155
155
|
MessageContentProcessorOrigin,
|
|
156
156
|
MessageContentProcessorCtxDTO,
|
|
157
157
|
MessageContentProcessorResultDTO,
|
|
158
|
+
WebSearchProviderDTO,
|
|
159
|
+
WebSearchSettingsDTO,
|
|
160
|
+
WebSearchResultDTO,
|
|
161
|
+
WebSearchDocumentDTO,
|
|
162
|
+
WebSearchRequestDTO,
|
|
163
|
+
WebSearchResponseDTO,
|
|
158
164
|
WorkerToHost,
|
|
159
165
|
HostToWorker,
|
|
160
166
|
} from "./api";
|
|
@@ -208,6 +214,47 @@ export type {
|
|
|
208
214
|
SpindleFrontendProcessRegistry,
|
|
209
215
|
} from "./dom";
|
|
210
216
|
|
|
217
|
+
export type {
|
|
218
|
+
SpindleMountedComponent,
|
|
219
|
+
SpindleComponentTarget,
|
|
220
|
+
SpindleConnectionKind,
|
|
221
|
+
SpindleConnectionRef,
|
|
222
|
+
SpindleTextInputOptions,
|
|
223
|
+
SpindleTextInputHandle,
|
|
224
|
+
SpindleTextAreaOptions,
|
|
225
|
+
SpindleTextAreaHandle,
|
|
226
|
+
SpindleNumericInputOptions,
|
|
227
|
+
SpindleNumericInputHandle,
|
|
228
|
+
SpindleNumberStepperOptions,
|
|
229
|
+
SpindleNumberStepperHandle,
|
|
230
|
+
SpindleCheckboxOptions,
|
|
231
|
+
SpindleCheckboxHandle,
|
|
232
|
+
SpindleSwitchOptions,
|
|
233
|
+
SpindleSwitchHandle,
|
|
234
|
+
SpindleSelectOption,
|
|
235
|
+
SpindleSelectOptionsBase,
|
|
236
|
+
SpindleSelectOptions,
|
|
237
|
+
SpindleSelectHandle,
|
|
238
|
+
SpindleMultiSelectOptions,
|
|
239
|
+
SpindleMultiSelectHandle,
|
|
240
|
+
SpindleModelComboboxOptions,
|
|
241
|
+
SpindleModelComboboxHandle,
|
|
242
|
+
SpindleFolderDropdownOptions,
|
|
243
|
+
SpindleFolderDropdownHandle,
|
|
244
|
+
SpindleBadgeColor,
|
|
245
|
+
SpindleBadgeOptions,
|
|
246
|
+
SpindleBadgeHandle,
|
|
247
|
+
SpindleSpinnerOptions,
|
|
248
|
+
SpindleSpinnerHandle,
|
|
249
|
+
SpindleCollapsibleSectionOptions,
|
|
250
|
+
SpindleCollapsibleSectionHandle,
|
|
251
|
+
SpindlePaginationOptions,
|
|
252
|
+
SpindlePaginationHandle,
|
|
253
|
+
SpindleCloseButtonOptions,
|
|
254
|
+
SpindleCloseButtonHandle,
|
|
255
|
+
SpindleComponentsHelper,
|
|
256
|
+
} from "./components";
|
|
257
|
+
|
|
211
258
|
export type { ExtensionInfo } from "./extension-info";
|
|
212
259
|
|
|
213
260
|
export type {
|
package/src/permissions.ts
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* links, consolidations) and long-term chat memory (vectorized
|
|
20
20
|
* chat-chunk retrieval, warmup, cache).
|
|
21
21
|
* - "macro_interceptor" — transform raw templates before macro parsing/dispatch
|
|
22
|
+
* - "web_search" — execute searches via the user's configured web search
|
|
23
|
+
* provider (e.g. SearXNG) and read the safe view of their
|
|
24
|
+
* web search settings (never the API key).
|
|
22
25
|
*/
|
|
23
26
|
export type SpindlePermission =
|
|
24
27
|
| "generation"
|
|
@@ -44,7 +47,8 @@ export type SpindlePermission =
|
|
|
44
47
|
| "image_gen"
|
|
45
48
|
| "images"
|
|
46
49
|
| "generation_parameters"
|
|
47
|
-
| "macro_interceptor"
|
|
50
|
+
| "macro_interceptor"
|
|
51
|
+
| "web_search";
|
|
48
52
|
|
|
49
53
|
export const ALL_PERMISSIONS: readonly SpindlePermission[] = [
|
|
50
54
|
"generation",
|
|
@@ -71,6 +75,7 @@ export const ALL_PERMISSIONS: readonly SpindlePermission[] = [
|
|
|
71
75
|
"images",
|
|
72
76
|
"generation_parameters",
|
|
73
77
|
"macro_interceptor",
|
|
78
|
+
"web_search",
|
|
74
79
|
] as const;
|
|
75
80
|
|
|
76
81
|
export function isValidPermission(p: string): p is SpindlePermission {
|
package/src/spindle-api.ts
CHANGED
|
@@ -103,6 +103,9 @@ import type {
|
|
|
103
103
|
MessageContentProcessorResultDTO,
|
|
104
104
|
SharedRpcRequestContextDTO,
|
|
105
105
|
SharedRpcEndpointPolicyDTO,
|
|
106
|
+
WebSearchRequestDTO,
|
|
107
|
+
WebSearchResponseDTO,
|
|
108
|
+
WebSearchSettingsDTO,
|
|
106
109
|
} from "./api";
|
|
107
110
|
import type {
|
|
108
111
|
ChatChunkDTO,
|
|
@@ -1370,6 +1373,74 @@ export interface SpindleAPI {
|
|
|
1370
1373
|
}>;
|
|
1371
1374
|
};
|
|
1372
1375
|
|
|
1376
|
+
/**
|
|
1377
|
+
* Web search (permission: `"web_search"`).
|
|
1378
|
+
*
|
|
1379
|
+
* Execute searches via the user's configured web search provider
|
|
1380
|
+
* (currently SearXNG; additional providers will be added over time) and
|
|
1381
|
+
* read the safe view of their web search settings. The host enforces all
|
|
1382
|
+
* upstream limits (engine list, max result count, max pages to scrape,
|
|
1383
|
+
* timeouts) — extensions cannot supply their own endpoint or API key.
|
|
1384
|
+
*
|
|
1385
|
+
* For user-scoped extensions, `userId` is inferred from the extension
|
|
1386
|
+
* owner. Operator-scoped extensions should pass the `userId` of the user
|
|
1387
|
+
* whose search engine should run the query.
|
|
1388
|
+
*
|
|
1389
|
+
* @example
|
|
1390
|
+
* ```ts
|
|
1391
|
+
* // Pre-flight: confirm web search is configured before issuing a query.
|
|
1392
|
+
* const settings = await spindle.webSearch.getSettings()
|
|
1393
|
+
* if (!settings.enabled) {
|
|
1394
|
+
* spindle.toast.warning('Configure a web search provider in Settings → Web Search first.')
|
|
1395
|
+
* return
|
|
1396
|
+
* }
|
|
1397
|
+
*
|
|
1398
|
+
* // Full enriched query — scrapes the top-N pages and returns a
|
|
1399
|
+
* // ready-to-inject context block.
|
|
1400
|
+
* const enriched = await spindle.webSearch.query({
|
|
1401
|
+
* query: 'latest LLM benchmark results',
|
|
1402
|
+
* count: 5,
|
|
1403
|
+
* })
|
|
1404
|
+
* await spindle.chat.appendMessage(chatId, {
|
|
1405
|
+
* role: 'system',
|
|
1406
|
+
* content: enriched.context ?? '',
|
|
1407
|
+
* })
|
|
1408
|
+
*
|
|
1409
|
+
* // Lightweight query — titles, URLs, snippets only.
|
|
1410
|
+
* const quick = await spindle.webSearch.query({
|
|
1411
|
+
* query: 'who won the 2026 world cup',
|
|
1412
|
+
* scrape: false,
|
|
1413
|
+
* })
|
|
1414
|
+
* for (const row of quick.results) {
|
|
1415
|
+
* spindle.log.info(`${row.title} — ${row.url}`)
|
|
1416
|
+
* }
|
|
1417
|
+
* ```
|
|
1418
|
+
*/
|
|
1419
|
+
webSearch: {
|
|
1420
|
+
/**
|
|
1421
|
+
* Run a search against the user's configured provider.
|
|
1422
|
+
*
|
|
1423
|
+
* Rejects with `"Web search is disabled"` when the user has not
|
|
1424
|
+
* configured a provider, and with `"Web search API URL is not configured"`
|
|
1425
|
+
* when the upstream endpoint is missing. Other upstream errors surface
|
|
1426
|
+
* as `Error("SearXNG returned HTTP NNN")` (or equivalent for future
|
|
1427
|
+
* providers).
|
|
1428
|
+
*
|
|
1429
|
+
* When `input.scrape` is `false`, `documents` and `context` are omitted
|
|
1430
|
+
* from the response. Otherwise the host scrapes up to
|
|
1431
|
+
* `WebSearchSettingsDTO.maxPagesToScrape` results, fills in
|
|
1432
|
+
* `documents[].content`, and assembles a prompt-ready `context` block.
|
|
1433
|
+
*/
|
|
1434
|
+
query(input: WebSearchRequestDTO): Promise<WebSearchResponseDTO>;
|
|
1435
|
+
/**
|
|
1436
|
+
* Read the safe view of the user's web search configuration. The raw
|
|
1437
|
+
* API key is never exposed — only `hasApiKey` indicates whether one is
|
|
1438
|
+
* on file. Useful for branching on `enabled` / `provider` /
|
|
1439
|
+
* `maxResultCount` before issuing a query.
|
|
1440
|
+
*/
|
|
1441
|
+
getSettings(userId?: string): Promise<WebSearchSettingsDTO>;
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1373
1444
|
/**
|
|
1374
1445
|
* Text editor (free tier — no permission needed).
|
|
1375
1446
|
* Opens the native Lumiverse expanded text editor modal on the user's
|