@lingxia/types 0.17.0 → 0.18.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 (43) hide show
  1. package/dist/automation/index.d.ts +104 -34
  2. package/dist/automation/index.d.ts.map +1 -1
  3. package/dist/error.d.ts +19 -0
  4. package/dist/error.d.ts.map +1 -1
  5. package/dist/error.js +23 -1
  6. package/dist/error.js.map +1 -1
  7. package/dist/esm/error.js +21 -0
  8. package/dist/esm/error.js.map +1 -1
  9. package/dist/esm/generated/error.js +1 -0
  10. package/dist/esm/generated/error.js.map +1 -1
  11. package/dist/esm/generated/i18n.js +10 -1
  12. package/dist/esm/generated/i18n.js.map +1 -1
  13. package/dist/esm/testing/public-api.js +72 -46
  14. package/dist/esm/testing/public-api.js.map +1 -1
  15. package/dist/generated/error.d.ts +4 -0
  16. package/dist/generated/error.d.ts.map +1 -1
  17. package/dist/generated/error.js +1 -0
  18. package/dist/generated/error.js.map +1 -1
  19. package/dist/generated/i18n.d.ts +1 -1
  20. package/dist/generated/i18n.d.ts.map +1 -1
  21. package/dist/generated/i18n.js +10 -1
  22. package/dist/generated/i18n.js.map +1 -1
  23. package/dist/generated/logic-web.d.ts +24 -3
  24. package/dist/generated/logic.d.ts +734 -378
  25. package/dist/generated/logic.d.ts.map +1 -1
  26. package/dist/index.d.ts +7 -5
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/process.d.ts +15 -4
  29. package/dist/process.d.ts.map +1 -1
  30. package/dist/testing/public-api.d.ts +70 -45
  31. package/dist/testing/public-api.d.ts.map +1 -1
  32. package/dist/testing/public-api.js +72 -46
  33. package/dist/testing/public-api.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/automation/index.ts +81 -33
  36. package/src/error.ts +23 -0
  37. package/src/generated/error.ts +1 -0
  38. package/src/generated/i18n.ts +10 -1
  39. package/src/generated/logic-web.d.ts +24 -3
  40. package/src/generated/logic.ts +790 -402
  41. package/src/index.ts +9 -6
  42. package/src/process.ts +14 -4
  43. package/src/testing/public-api.ts +80 -47
@@ -5,9 +5,21 @@
5
5
  // by external Rong modules in this generated prelude.
6
6
  declare const appDownloadPathBrand: unique symbol;
7
7
  declare const systemDownloadsPathBrand: unique symbol;
8
+ /** Managed paths and relative userdata paths; system Downloads references are excluded. */
9
+ export type ManagedPath = string & { readonly [systemDownloadsPathBrand]?: never };
8
10
 
9
- export interface PageConfig<TData extends Record<string, unknown> = Record<string, unknown>> {
10
- data?: TData;
11
+ /** JSON-shaped state; undefined removes a property in setData. */
12
+ export type PageDataValue<T> = unknown extends T ? T : T extends string | number | boolean | null | undefined ? T
13
+ : T extends (...args: never[]) => unknown ? never
14
+ : T extends object ? { [K in keyof T]: PageDataValue<T[K]> } : never;
15
+
16
+ export type NoReservedPageMembers<T> = {
17
+ [K in keyof T]: K extends 'data' ? T[K]
18
+ : K extends keyof PageInstance | '_setData' | '_cancelPendingSetData' ? never : T[K];
19
+ };
20
+
21
+ export interface PageConfig<TData extends object = Record<string, unknown>> {
22
+ data?: TData & PageDataValue<TData>;
11
23
  onLoad?: (options?: PageLoadOptions) => void | Promise<void>;
12
24
  onShow?: () => void | Promise<void>;
13
25
  onReady?: () => void | Promise<void>;
@@ -46,31 +58,98 @@ export type NoLifecycleTypos<TCustom, TNames extends string> = {
46
58
 
47
59
  /**
48
60
  * A `setData` key that addresses inside `data` — `'a.b'` or `'rows[0].name'`.
49
- * Values behind a path stay `unknown`: the runtime resolves the path, so the
50
- * type cannot.
61
+ * Kept for documentation; nested writes go through `setPath` (checked) or
62
+ * `setDataPath` (unchecked). `setData` no longer accepts these keys.
51
63
  */
52
64
  export type PageDataPath = `${string}.${string}` | `${string}[${number}]${string}`;
53
65
 
54
66
  /**
55
- * A field initialized to `null` or `[]` states nothing about what will fill it
56
- * later, so it stays open. Annotate it (`null as Profile | null`) to have the
57
- * fill checked.
58
- */
59
- export type LazyInitField<T> = [T] extends [null | undefined]
60
- ? unknown
61
- : [T] extends [never[]]
62
- ? unknown[]
63
- : T;
64
-
65
- /**
66
- * Top-level keys are checked against `data`; only path-shaped keys stay open.
67
- * A misspelled or wrongly typed top-level key is a compile error.
68
- */
69
- export type SetDataPatch<TData> = { [K in keyof TData]?: LazyInitField<TData[K]> } &
70
- Partial<Record<PageDataPath, unknown>>;
71
-
72
- export interface PageInstance<TData extends Record<string, unknown> = Record<string, unknown>> {
73
- data: TData;
67
+ * @deprecated Page `data` is no longer widened for `null` / `[]` initializers.
68
+ * Annotate the field (`null as Profile | null`) when a later fill should be
69
+ * checked.
70
+ */
71
+ export type LazyInitField<T> = T;
72
+
73
+ type PageReadonlyDepth = [never, 0, 1, 2, 3, 4, 5, 6];
74
+
75
+ type PagePrimitive = string | number | boolean | bigint | symbol | null | undefined;
76
+
77
+ /**
78
+ * Deep readonly view of page `data`. Static only — the runtime object is not
79
+ * frozen.
80
+ */
81
+ export type DeepReadonly<T, D extends number = 6> = [D] extends [never]
82
+ ? T
83
+ : T extends PagePrimitive
84
+ ? T
85
+ : T extends Function
86
+ ? T
87
+ : T extends readonly unknown[]
88
+ ? { readonly [K in keyof T]: DeepReadonly<T[K], PageReadonlyDepth[D]> }
89
+ : T extends object
90
+ ? { readonly [K in keyof T]: DeepReadonly<T[K], PageReadonlyDepth[D]> }
91
+ : T;
92
+
93
+ /**
94
+ * Tuple path into `data`, depth-capped so large page states stay completable.
95
+ */
96
+ export type DataPath<T, D extends number = 5> = [D] extends [never]
97
+ ? never
98
+ : T extends readonly (infer U)[]
99
+ ? [number] | [number, ...DataPath<U, PageReadonlyDepth[D]>]
100
+ : T extends object
101
+ ? {
102
+ [K in keyof T & (string | number)]:
103
+ | [K]
104
+ | (DataPath<T[K], PageReadonlyDepth[D]> extends infer Rest
105
+ ? Rest extends readonly PropertyKey[]
106
+ ? [K, ...Rest]
107
+ : never
108
+ : never);
109
+ }[keyof T & (string | number)]
110
+ : never;
111
+
112
+ export type DataPathValue<T, P extends readonly PropertyKey[]> = T extends null | undefined
113
+ ? never
114
+ : T extends unknown
115
+ ? P extends readonly [
116
+ infer K,
117
+ ...infer Rest,
118
+ ]
119
+ ? Rest extends readonly PropertyKey[]
120
+ ? Rest['length'] extends 0
121
+ ? K extends keyof T
122
+ ? T[K]
123
+ : K extends number
124
+ ? T extends readonly (infer U)[]
125
+ ? U
126
+ : never
127
+ : never
128
+ : K extends keyof T
129
+ ? DataPathValue<T[K], Rest>
130
+ : K extends number
131
+ ? T extends readonly (infer U)[]
132
+ ? DataPathValue<U, Rest>
133
+ : never
134
+ : never
135
+ : never
136
+ : never
137
+ : never;
138
+
139
+ /**
140
+ * Top-level keys are checked against `data`. Nested writes use `setPath` or
141
+ * the unchecked `setDataPath`.
142
+ */
143
+ export type SetDataPatch<TData> = { [K in keyof TData]?: TData[K] };
144
+
145
+ type SetDataValue<TData, K> = K extends PageDataPath
146
+ ? { "LingXia type error": "use setPath or setDataPath for nested writes" }
147
+ : K extends keyof TData
148
+ ? TData[K] | DeepReadonly<TData[K]>
149
+ : { "LingXia type error": "unknown data key" };
150
+
151
+ export interface PageInstance<TData extends object = Record<string, unknown>> {
152
+ readonly data: { readonly [K in keyof TData]: DeepReadonly<TData[K]> };
74
153
  route: string;
75
154
  /**
76
155
  * Available when this page was opened as a surface via
@@ -81,7 +160,17 @@ export interface PageInstance<TData extends Record<string, unknown> = Record<str
81
160
  * Available when this page was opened by `lx.navigateTo(...)`.
82
161
  */
83
162
  opener?: PageMessagePort;
84
- setData(data: SetDataPatch<TData>, callback?: () => void): void;
163
+ setData<TPatch extends Record<string, unknown>>(
164
+ data: TPatch & { [K in keyof TPatch]: SetDataValue<TData, K> },
165
+ ): void;
166
+ setPath<const P extends DataPath<TData>>(
167
+ path: P,
168
+ value: DataPathValue<TData, P>,
169
+ ): void;
170
+ /** Nested write by a runtime-resolved string path. JSON shape is checked; the path/value relationship is not. */
171
+ setDataPath(path: string, value: JsonValue | undefined): void;
172
+ /** Drain pending state writes; an attached View acknowledges application, not paint. Rejects on unload. */
173
+ flush(): Promise<void>;
85
174
  }
86
175
 
87
176
  /**
@@ -98,6 +187,12 @@ export interface StreamHandle<T = unknown> {
98
187
  end(result?: unknown): void;
99
188
  /** End the stream with an error. */
100
189
  error(code: string, message?: string): void;
190
+ /**
191
+ * Called once if View cancels before `end`/`error`. Returns unsubscribe.
192
+ * The generator form still observes cancel in `finally`; use this for the
193
+ * callback-based handle.
194
+ */
195
+ onCancel(handler: () => void): () => void;
101
196
  }
102
197
 
103
198
  /**
@@ -111,9 +206,9 @@ export interface ChannelHandle<TSend = unknown, TReceive = unknown> {
111
206
  send(payload: TSend): void;
112
207
  /** Close the channel from Logic side. */
113
208
  close(code?: string, reason?: string): void;
114
- /** Register a listener for incoming events. */
115
- on(event: 'data', handler: (payload: TReceive) => void): void;
116
- on(event: 'close', handler: (info: { code: string; reason: string }) => void): void;
209
+ /** Register a listener for incoming events. Returns unsubscribe. */
210
+ on(event: 'data', handler: (payload: TReceive) => void): () => void;
211
+ on(event: 'close', handler: (info: { code: string; reason: string }) => void): () => void;
117
212
  }
118
213
 
119
214
  /**
@@ -132,36 +227,54 @@ export type DownloadOptions<TDestination extends DownloadDestination = DownloadD
132
227
  export type DownloadResultForDestination<TDestination extends DownloadDestination> =
133
228
  TDestination extends 'downloads' ? DownloadsDownloadResult : AppDownloadResult;
134
229
 
135
- export interface DownloadProgressEvent<TResult extends DownloadResult = DownloadResult> {
136
- kind: 'progress' | 'paused' | 'resumed' | 'canceled' | 'completed';
137
- downloadedBytes?: number;
138
- totalBytes?: number;
139
- /** Present only when the total size is known. */
140
- progress?: number;
141
- result?: TResult;
230
+ export type DownloadProgressEvent<TResult extends DownloadResult = DownloadResult> =
231
+ | {
232
+ kind: 'progress' | 'paused' | 'resumed';
233
+ downloadedBytes?: number;
234
+ totalBytes?: number;
235
+ /** Present only when the total size is known. */
236
+ progress?: number;
237
+ }
238
+ | {
239
+ kind: 'canceled';
240
+ downloadedBytes?: number;
241
+ totalBytes?: number;
242
+ progress?: number;
243
+ }
244
+ | {
245
+ kind: 'completed';
246
+ downloadedBytes?: number;
247
+ totalBytes?: number;
248
+ progress?: number;
249
+ result: TResult;
250
+ };
251
+
252
+ export type DownloadIteratorResult<TResult extends DownloadResult = DownloadResult> =
253
+ IteratorResult<DownloadProgressEvent<TResult>, void>;
254
+
255
+ export type AppTempDownloadResult = Extract<AppDownloadResult, { storage: 'temp' }>;
256
+ export type AppPersistedDownloadResult = Extract<AppDownloadResult, { storage: 'userdata' }>;
257
+
258
+ export type ChooseFileSingleResult = {
259
+ status: 'ok';
260
+ paths: [string];
261
+ } | CanceledResult;
262
+
263
+ /** A running operation. Progress has one consumer; stopping observation does not cancel it. */
264
+ export interface Task<TResult, TProgress> {
265
+ readonly result: Promise<TResult>;
266
+ readonly progress: AsyncIterable<TProgress>;
142
267
  }
143
268
 
144
- export interface DownloadIteratorResult<TResult extends DownloadResult = DownloadResult> {
145
- done: boolean;
146
- value?: DownloadProgressEvent<TResult>;
269
+ export interface CancelableTask<TResult, TProgress> extends Task<TResult, TProgress> {
270
+ /** Requests cancellation. Await result to observe the terminal outcome. */
271
+ cancel(): Promise<void>;
147
272
  }
148
273
 
149
- export interface DownloadTask<TDownloadResult extends DownloadResult = DownloadResult>
150
- extends PromiseLike<TDownloadResult>,
151
- AsyncIterable<DownloadProgressEvent<TDownloadResult>> {
152
- next(): Promise<DownloadIteratorResult<TDownloadResult>>;
153
- /** Stops iteration only. Does not cancel the underlying download task. */
154
- return(): Promise<DownloadIteratorResult<TDownloadResult>>;
155
- catch<TRejected = never>(
156
- onrejected?: ((reason: unknown) => TRejected | PromiseLike<TRejected>) | null,
157
- ): Promise<TDownloadResult | TRejected>;
158
- finally(onfinally?: (() => void) | null): Promise<TDownloadResult>;
274
+ export interface DownloadTask<TResult extends DownloadResult = DownloadResult>
275
+ extends CancelableTask<TResult, DownloadProgressEvent<TResult>> {
159
276
  pause(): Promise<void>;
160
277
  resume(): Promise<void>;
161
- cancel(): Promise<void>;
162
- /** Alias for cancel(), matching browser/mini-program abort naming. */
163
- abort(): Promise<void>;
164
- wait(): Promise<TDownloadResult>;
165
278
  }
166
279
 
167
280
  declare global {
@@ -188,11 +301,24 @@ declare global {
188
301
 
189
302
  /**
190
303
  * Launch-at-startup control. Absent where the host cannot register a
191
- * startup item; its presence and `lx.supports({ capability: 'autostart' })` always
192
- * agree, so `lx.app.autostart?.…` and the query are interchangeable.
304
+ * startup item; its presence and `lx.supports('app.autostart')` always
305
+ * agree, so `lx.host.autostart?.…` and the query are interchangeable.
193
306
  */
194
307
  autostart?: AutostartApi;
195
308
 
309
+ /**
310
+ * Local notifications. Absent where the host cannot post them; its presence
311
+ * and `lx.supports('app.notification')` always agree.
312
+ */
313
+ notification?: NotificationApi;
314
+
315
+ /**
316
+ * Product-drawn desktop banner (top-right). Absent off desktop and in
317
+ * guest lxapps; its presence and `lx.supports('app.banner')`
318
+ * always agree.
319
+ */
320
+ banner?: BannerApi;
321
+
196
322
  /** The language this lxapp renders in. Every lxapp follows it. */
197
323
  readonly displayLanguage: DisplayLanguageApi;
198
324
 
@@ -201,19 +327,25 @@ declare global {
201
327
 
202
328
  /**
203
329
  * Product-wide settings, and their single writer. Present only in the
204
- * Control app the host sealed at build time; its presence and
205
- * `lx.supports({ capability: 'control' })` always agree, so
206
- * `lx.app.control?.…` and the query are interchangeable.
330
+ * Control app the host sealed at build time. Use
331
+ * `lx.host.control !== undefined` to inspect that identity.
207
332
  */
208
333
  readonly control?: ControlApi;
209
334
 
210
335
  /**
211
336
  * Product-wide cache reporting and clearing for a settings screen.
212
- * Present only in the Control app; its presence and
213
- * `lx.supports({ capability: 'control' })` always agree, so
214
- * `lx.app.cache?.…` and the query are interchangeable.
337
+ * Present only in the Control app; presence agrees with
338
+ * `lx.host.control !== undefined`.
215
339
  */
216
340
  cache?: AppCacheApi;
341
+
342
+ /**
343
+ * Take over host updates for the rest of this process. Irreversible: the
344
+ * built-in auto-flow will not prompt or download again, including after
345
+ * the calling page unloads. Does not cancel an already-started update task.
346
+ * `update.apply()` claims as well. `checkUpdate()` does not.
347
+ */
348
+ claimCustomUpdate(): void;
217
349
  }
218
350
 
219
351
  /** Runtime environment constants backed by abstract `lx://` paths. */
@@ -223,18 +355,36 @@ declare global {
223
355
  /**
224
356
  * Terminal product settings. Present only in the host-bundled Terminal
225
357
  * Settings lxapp when the host declares `capabilities.terminal`; its
226
- * presence and `lx.supports({ capability: 'terminal' })` always agree.
358
+ * presence and `lx.supports('terminal')` always agree.
227
359
  */
228
360
  readonly terminal?: TerminalApi;
229
361
 
230
362
  /** Download to the downloads directory. */
231
363
  downloadFile(options: DownloadsDownloadOptions): DownloadTask<DownloadsDownloadResult>;
364
+ /** Download to a durable app-owned path. */
365
+ downloadFile(
366
+ options: AppDownloadOptions & { filePath: string },
367
+ ): DownloadTask<AppPersistedDownloadResult>;
368
+ /** Download to a temporary app-owned path. */
369
+ downloadFile(
370
+ options: AppDownloadOptions & { filePath?: undefined },
371
+ ): DownloadTask<AppTempDownloadResult>;
232
372
  /** Download to the lxapp-managed app directory. */
233
373
  downloadFile(options: AppDownloadOptions): DownloadTask<AppDownloadResult>;
234
374
  /** Download with a destination-correlated result type. */
235
375
  downloadFile<TDestination extends DownloadDestination = "app">(
236
376
  options: DownloadOptions<TDestination>,
237
377
  ): DownloadTask<DownloadResultForDestination<TDestination>>;
378
+ /** Single-file picker: a completed selection is exactly one path. */
379
+ chooseFile(options: ChooseFileOptions & { multiple: false }): Promise<ChooseFileSingleResult>;
380
+ chooseFile(options: ChooseFileOptions & { multiple: true }): Promise<ChooseFileResult>;
381
+ /**
382
+ * Opens a file picker.
383
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the picker. A
384
+ * completed selection resolves `{ status: 'ok', paths }` with at least one
385
+ * path. Rejects when the picker fails or returns an invalid payload.
386
+ */
387
+ chooseFile(options?: ChooseFileOptions): Promise<ChooseFileResult>;
238
388
 
239
389
  /**
240
390
  * Open this lxapp's store with every key's shape pinned on the handle.
@@ -255,7 +405,7 @@ export type StorageSchema = object;
255
405
 
256
406
  type StorageKey<S extends object> = Extract<keyof S, string>;
257
407
  type StorageEntry<S extends object> = {
258
- [K in StorageKey<S>]: [key: K, value: S[K]];
408
+ [K in StorageKey<S>]: [key: K, value: S[K] | DeepReadonly<S[K]>];
259
409
  }[StorageKey<S>];
260
410
 
261
411
  /**
@@ -269,7 +419,7 @@ type StorageEntry<S extends object> = {
269
419
  * unchecked assertion this type exists to remove from `get<T>()`.
270
420
  */
271
421
  export type TypedStorage<S extends object> = {
272
- get<K extends StorageKey<S>>(key: K): Promise<S[K] | undefined>;
422
+ get<K extends StorageKey<S>>(key: K, decode?: (value: unknown) => S[K]): Promise<S[K] | undefined>;
273
423
  set(...entry: StorageEntry<S>): Promise<void>;
274
424
  has(key: StorageKey<S>): Promise<boolean>;
275
425
  delete(key: StorageKey<S>): Promise<void>;
@@ -279,15 +429,18 @@ export type TypedStorage<S extends object> = {
279
429
  };
280
430
 
281
431
  /**
282
- * Result of `lx.showActionSheet`. Branch on `canceled` before reading
283
- * the selected item index.
432
+ * Result of `lx.showActionSheet`. Branch on `status` before reading
433
+ * the selected item id.
284
434
  */
285
435
  export type ActionSheetResult = {
286
- canceled: false;
287
- /** Index of the tapped item in `itemList`. */
288
- index: number;
436
+ status: 'ok';
437
+ /** Stable id of the selected action. */
438
+ id: string;
289
439
  } | CanceledResult;
290
440
 
441
+ /** An acknowledgement dialog has no cancel button. */
442
+ export type AlertOptions = Omit<ShowModalOptions, 'showCancel' | 'cancelText' | 'cancelColor'>;
443
+
291
444
  /** Every surface handle, narrowable by `kind`. */
292
445
  export type AnySurface = PageSurface | DeclaredSurface | AppSurface | TabSurface | BuiltinSurface;
293
446
 
@@ -295,7 +448,7 @@ export type AnySurface = PageSurface | DeclaredSurface | AppSurface | TabSurface
295
448
  * The product-wide cache a settings screen reports and clears.
296
449
  * App-scoped, not lxapp-scoped: the figure covers every lxapp the host
297
450
  * has run. Injected only into the Control app, same gate as
298
- * `lx.app.control` — guests do not have the member.
451
+ * `lx.host.control` — guests do not have the member.
299
452
  */
300
453
  export type AppCacheApi = {
301
454
  /** Estimated reclaimable managed bytes; excludes live session storage and WebView cache. */
@@ -333,7 +486,7 @@ export type AppDownloadOptions = DownloadOptionsBase & {
333
486
  /**
334
487
  * Optional app-owned durable output path.
335
488
  *
336
- * Omit `filePath` to receive a temporary result in `tempFilePath`. Relative
489
+ * Omit `filePath` to receive a temporary result in `uri`. Relative
337
490
  * paths resolve under user data. `lx://` paths must target `lx://userdata`;
338
491
  * `lx://usercache` is not accepted here.
339
492
  */
@@ -345,24 +498,15 @@ export type AppDownloadOptions = DownloadOptionsBase & {
345
498
  };
346
499
 
347
500
  export type AppDownloadResult = {
348
- /**
349
- * Temporary result.
350
- *
351
- * Not durable; move or copy it to `lx://userdata` if you need to keep it.
352
- *
353
- * When `filePath` is omitted, the runtime must be able to infer a file
354
- * type from the URL or the server's `Content-Type` header.
355
- */
356
- tempFilePath: string;
357
- filePath?: never;
501
+ uri: AppDownloadFilePath;
502
+ storage: 'temp';
358
503
  mimeType?: string;
359
- size: number;
504
+ sizeBytes: number;
360
505
  } | {
361
- /** Durable destination under `lx://userdata`. */
362
- filePath: AppDownloadFilePath;
363
- tempFilePath?: never;
506
+ uri: AppDownloadFilePath;
507
+ storage: 'userdata';
364
508
  mimeType?: string;
365
- size: number;
509
+ sizeBytes: number;
366
510
  };
367
511
 
368
512
  export type AppInstance = AppConfig & {
@@ -412,7 +556,7 @@ export type AppScreenshotOptions = {
412
556
 
413
557
  export type AppScreenshotResult = {
414
558
  /** `lx://` URI of the captured PNG in the lxapp temp directory. */
415
- tempFilePath: string;
559
+ uri: string;
416
560
  /** Image width in pixels, when the runtime could read it from the PNG. */
417
561
  width?: number;
418
562
  /** Image height in pixels, when the runtime could read it from the PNG. */
@@ -425,7 +569,7 @@ export type AppSurface = SurfaceBase & SurfaceShowable & {
425
569
  readonly realized: 'main' | 'aside';
426
570
  };
427
571
 
428
- /** `lx.app.appearance` — the scheme this lxapp renders in. */
572
+ /** `lx.host.appearance` — the scheme this lxapp renders in. */
429
573
  export type AppearanceApi = {
430
574
  /**
431
575
  * The scheme this lxapp is rendering in. An lxapp that pinned one in its
@@ -447,25 +591,6 @@ export type AppearanceApi = {
447
591
  */
448
592
  export type AppearancePreference = 'auto' | 'light' | 'dark';
449
593
 
450
- /**
451
- * Launch-at-startup control for the host app.
452
- * Absent (`undefined`) wherever the host cannot register a startup item.
453
- * `lx.supports({ capability: 'autostart' })` and the member's presence always
454
- * agree, so either gate works:
455
- * ```ts
456
- * if (lx.supports({ capability: 'autostart' })) {
457
- * // render the "Launch at startup" toggle
458
- * }
459
- * ```
460
- * Requires `capabilities.autostart: true` in `lingxia.yaml`; without it the
461
- * member is absent on all platforms. Declaring the capability never enables
462
- * autostart by itself — the SDK registers the app only when `setEnabled(true)`
463
- * is called, so the decision stays with the user (typically a settings-page
464
- * toggle, default off).
465
- * Host-app-level capability: like `checkUpdate` and `screenshot`, the methods
466
- * are available only to the native-assigned Control app; other lxapps receive
467
- * a permission error.
468
- */
469
594
  export type AutostartApi = {
470
595
  /**
471
596
  * Whether the app is currently registered to launch at startup, read from
@@ -483,6 +608,36 @@ export type AutostartApi = {
483
608
  setEnabled(on: boolean): Promise<void>;
484
609
  };
485
610
 
611
+ /**
612
+ * Product-drawn desktop banner, top-right. Not an OS notification and
613
+ * not bound to App Link. Present only in the desktop Control app;
614
+ * presence and `lx.supports('app.banner')` always agree.
615
+ * No buttons: an informational card that auto-dismisses (5s unless
616
+ * `timeoutMs` is set). With buttons: a gate that waits for a choice,
617
+ * dismiss, timeout, or replace. User outcomes resolve; presentation
618
+ * failures reject.
619
+ */
620
+ export type BannerApi = {
621
+ show(options: {
622
+ id?: string;
623
+ title: string;
624
+ body?: string;
625
+ actions?: Array<{
626
+ id: string;
627
+ label: string;
628
+ style?: 'default' | 'primary' | 'destructive';
629
+ }>;
630
+ timeoutMs?: number;
631
+ /** Omit/`system` follows the OS. `light`/`dark` force chrome. `#RGB`/`#RRGGBB`/`#RRGGBBAA` is a solid fill. */
632
+ background?: 'system' | 'light' | 'dark' | string;
633
+ }): Promise<
634
+ | { status: 'ok'; id: string; action: string }
635
+ | { status: 'canceled'; id: string; reason: 'dismissed' | 'timeout' | 'replaced' }
636
+ >;
637
+ /** Unknown ids are fine. */
638
+ dismiss(id: string): Promise<void>;
639
+ };
640
+
486
641
  export type BinaryFileData = ArrayBuffer | ArrayBufferView;
487
642
 
488
643
  /**
@@ -494,16 +649,15 @@ export type BuiltinShellPage = 'downloads';
494
649
  /**
495
650
  * A host builtin page such as downloads. The shell owns
496
651
  * its lifetime and its visibility, so this handle reports identity:
497
- * there is no `show` / `hide`, and the inherited `close()` rejects
498
- * with `unsupported_placement`.
652
+ * there is no `show`, `hide`, `close`, or `onClose`.
499
653
  */
500
- export type BuiltinSurface = SurfaceBase & {
654
+ export type BuiltinSurface = Omit<SurfaceBase, 'close' | 'onClose'> & {
501
655
  readonly kind: 'builtin';
502
656
  };
503
657
 
504
658
  /** The user dismissed the operation. Never an error. */
505
659
  export type CanceledResult = {
506
- canceled: true;
660
+ status: 'canceled';
507
661
  };
508
662
 
509
663
  export type ChooseDirectoryOptions = {
@@ -512,11 +666,11 @@ export type ChooseDirectoryOptions = {
512
666
  };
513
667
 
514
668
  /**
515
- * Result of `lx.chooseDirectory`. Branch on `canceled` before reading
669
+ * Result of `lx.chooseDirectory`. Branch on `status` before reading
516
670
  * the selected directory.
517
671
  */
518
672
  export type ChooseDirectoryResult = {
519
- canceled: false;
673
+ status: 'ok';
520
674
  /** Native-consumable directory reference (path or URI). */
521
675
  path: string;
522
676
  } | CanceledResult;
@@ -536,11 +690,11 @@ export type ChooseFileOptions = {
536
690
  };
537
691
 
538
692
  /**
539
- * Result of `lx.chooseFile`. Branch on `canceled` before reading the
693
+ * Result of `lx.chooseFile`. Branch on `status` before reading the
540
694
  * selected paths.
541
695
  */
542
696
  export type ChooseFileResult = {
543
- canceled: false;
697
+ status: 'ok';
544
698
  /**
545
699
  * File paths returned by LingXia; always at least one. Values may be
546
700
  * app-local paths, `lx://...` paths, or platform system-picker references.
@@ -555,25 +709,89 @@ export type ChooseMediaOptions = {
555
709
  mediaType?: ('image' | 'video')[];
556
710
  sourceType?: ('album' | 'camera')[];
557
711
  camera?: 'back' | 'front';
558
- maxDuration?: number;
712
+ maxDurationSeconds?: number;
559
713
  };
560
714
 
561
715
  /**
562
- * Result of `lx.chooseMedia`. Branch on `canceled` before reading the
716
+ * Result of `lx.chooseMedia`. Branch on `status` before reading the
563
717
  * selected entries.
564
718
  */
565
719
  export type ChooseMediaResult = {
566
- canceled: false;
720
+ status: 'ok';
567
721
  /** Picked media; always at least one entry. */
568
722
  entries: [ChosenMediaEntry, ...ChosenMediaEntry[]];
569
723
  } | CanceledResult;
570
724
 
571
725
  export type ChosenMediaEntry = {
572
- tempFilePath: string;
726
+ uri: string;
573
727
  fileType: 'image' | 'video';
574
728
  isOriginal: boolean;
575
729
  };
576
730
 
731
+ export type ClipboardApi = globalThis.ClipboardApi;
732
+
733
+ export type ClipboardItem = {
734
+ type: 'text';
735
+ text: string;
736
+ } | {
737
+ type: 'image';
738
+ /**
739
+ * Temporary `lx://temp` PNG, session-scoped and auto-cleaned. Move or
740
+ * copy it with `lx.fs` if you need to keep it.
741
+ */
742
+ filePath: string;
743
+ };
744
+
745
+ export type ClipboardReadOptions = {
746
+ /** Omit to receive every representation the host can surface. */
747
+ type?: ClipboardType;
748
+ };
749
+
750
+ /**
751
+ * Result of `lx.clipboard.read`. Omit `type` to receive every
752
+ * representation this host can surface. A requested type that is
753
+ * absent is `{ status: 'empty' }`, not an error.
754
+ */
755
+ export type ClipboardReadResult = {
756
+ status: 'empty';
757
+ } | {
758
+ status: 'ok';
759
+ items: ClipboardItem[];
760
+ } | CanceledResult;
761
+
762
+ /**
763
+ * Result of `lx.clipboard.readText`. Branch on `status`.
764
+ * A copied empty string is `{ status: 'ok', text: '' }`;
765
+ * an image-only clipboard is `{ status: 'empty' }`.
766
+ */
767
+ export type ClipboardTextResult = {
768
+ status: 'empty';
769
+ } | {
770
+ status: 'ok';
771
+ text: string;
772
+ } | CanceledResult;
773
+
774
+ /** Representations the runtime can round-trip. Closed union. */
775
+ export type ClipboardType = 'text' | 'image';
776
+
777
+ /**
778
+ * One clipboard write. Every host accepts both: `image` takes a PNG or
779
+ * JPEG file and re-encodes it as the platform's native image format.
780
+ */
781
+ export type ClipboardWriteItem = {
782
+ type: 'text';
783
+ /** Unicode text. Rejects `E_INVALID_ARG` when larger than 1 MiB. */
784
+ text: string;
785
+ } | {
786
+ type: 'image';
787
+ /**
788
+ * Managed `lx://` path, or a picker result from `lx.chooseFile` /
789
+ * `lx.chooseMedia` — the same file rules as `lx.share`. Rejects
790
+ * `E_INVALID_ARG` when the file is not a decodable image.
791
+ */
792
+ filePath: string;
793
+ };
794
+
577
795
  export type CompressImageOptions = {
578
796
  path: string;
579
797
  quality?: number;
@@ -582,27 +800,34 @@ export type CompressImageOptions = {
582
800
  };
583
801
 
584
802
  export type CompressImageResult = {
585
- tempFilePath: string;
803
+ uri: string;
586
804
  };
587
805
 
588
- export type CompressVideoIteratorResult = {
589
- done: boolean;
590
- value?: CompressVideoProgressEvent;
591
- };
806
+ export type CompressVideoIteratorResult = IteratorResult<CompressVideoProgressEvent, void>;
592
807
 
593
808
  export type CompressVideoOptions = {
809
+ signal?: AbortSignal;
594
810
  /**
595
811
  * Source video path or `lx://` URI.
596
812
  */
597
813
  path: string;
598
814
  /**
599
- * Cross-platform note: video compression parameters are best-effort and may map to
600
- * native presets instead of exact encoder settings.
601
- *
815
+ * Optional output path for compressed file.
816
+ */
817
+ outputPath?: string;
818
+ } & (
819
+ | {
820
+ /**
602
821
  * Compression quality preset.
603
- * When provided, `bitrate`, `fps`, and `resolution` are ignored.
822
+ * Mutually exclusive with `bitrate`, `fps`, and `resolution`.
604
823
  */
605
- quality?: VideoCompressQuality;
824
+ quality: VideoCompressQuality;
825
+ bitrate?: never;
826
+ fps?: never;
827
+ resolution?: never;
828
+ }
829
+ | {
830
+ quality?: never;
606
831
  /**
607
832
  * Preferred target video bitrate in kbps.
608
833
  * May be adjusted or ignored by platform codec/runtime limitations.
@@ -618,11 +843,8 @@ export type CompressVideoOptions = {
618
843
  * May be approximated or ignored by platform transcoder capabilities.
619
844
  */
620
845
  resolution?: number;
621
- /**
622
- * Optional output path for compressed file.
623
- */
624
- outputPath?: string;
625
- };
846
+ }
847
+ );
626
848
 
627
849
  export type CompressVideoProgressEvent = {
628
850
  /** Transcode progress in percent, `0`-`100`. */
@@ -630,7 +852,7 @@ export type CompressVideoProgressEvent = {
630
852
  };
631
853
 
632
854
  export type CompressVideoResult = {
633
- tempFilePath: string;
855
+ uri: string;
634
856
  width: number;
635
857
  height: number;
636
858
  durationMs: number;
@@ -644,23 +866,11 @@ export type CompressVideoResult = {
644
866
 
645
867
  /**
646
868
  * Handle returned by `lx.compressVideo`.
647
- * Awaiting the task resolves with the final {@link CompressVideoResult}.
648
- * Iterating it with `for await` yields {@link CompressVideoProgressEvent}s
869
+ * Awaiting `task.result` resolves with the final {@link CompressVideoResult}.
870
+ * Iterating `task.progress` with `for await` yields {@link CompressVideoProgressEvent}s
649
871
  * while the transcode runs.
650
872
  */
651
- export type CompressVideoTask = PromiseLike<CompressVideoResult> & AsyncIterable<CompressVideoProgressEvent> & {
652
- next(): Promise<CompressVideoIteratorResult>;
653
- /** Stops iteration only. Does not cancel the compression. */
654
- return(): Promise<CompressVideoIteratorResult>;
655
- catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<CompressVideoResult | TResult>;
656
- finally(onfinally?: (() => void) | null): Promise<CompressVideoResult>;
657
- /**
658
- * Cancels the transcode and deletes any partial output.
659
- * The task promise rejects with an `AbortError` (`code: 'E_ABORT'`).
660
- */
661
- cancel(): void;
662
- wait(): Promise<CompressVideoResult>;
663
- };
873
+ export type CompressVideoTask = CancelableTask<CompressVideoResult, CompressVideoProgressEvent>;
664
874
 
665
875
  /**
666
876
  * Configured page name from `lxapp.json` / `lingxia.yaml`. JavaScript
@@ -673,17 +883,20 @@ export type CompressVideoTask = PromiseLike<CompressVideoResult> & AsyncIterable
673
883
  */
674
884
  export type ConfiguredPageName = keyof LxAppPages extends never ? string : keyof LxAppPages;
675
885
 
886
+ /** A confirmation dialog always permits declining. */
887
+ export type ConfirmOptions = Omit<ShowModalOptions, 'showCancel'>;
888
+
676
889
  export type ConnectWifiOptions = {
677
- SSID: string;
890
+ ssid: string;
678
891
  password?: string;
679
892
  };
680
893
 
681
894
  /**
682
- * `lx.app.control` — product-wide settings, and their single writer.
895
+ * `lx.host.control` — product-wide settings, and their single writer.
683
896
  * Present only in the Control app. Bind it once rather than repeating
684
- * `lx.app.control!`:
897
+ * `lx.host.control!`:
685
898
  * ```js
686
- * const control = lx.app.control;
899
+ * const control = lx.host.control;
687
900
  * if (!control) return; // not the Control app
688
901
  * await control.appearance.setPreference('dark');
689
902
  * ```
@@ -693,7 +906,7 @@ export type ControlApi = {
693
906
  readonly appearance: ControlAppearanceApi;
694
907
  };
695
908
 
696
- /** `lx.app.control.appearance` — the product's own light/dark setting. */
909
+ /** `lx.host.control.appearance` — the product's own light/dark setting. */
697
910
  export type ControlAppearanceApi = {
698
911
  /** What the user chose for the whole product. */
699
912
  getPreference(): AppearancePreference;
@@ -704,7 +917,7 @@ export type ControlAppearanceApi = {
704
917
  setPreference(preference: AppearancePreference): Promise<void>;
705
918
  /**
706
919
  * Follow the choice, not what it resolves to: a system flip under `'auto'`
707
- * moves `lx.app.appearance.watch` and leaves this quiet. Starts with the
920
+ * moves `lx.host.appearance.watch` and leaves this quiet. Starts with the
708
921
  * current value; that first callback runs synchronously, before
709
922
  * `watchPreference` returns.
710
923
  */
@@ -712,7 +925,7 @@ export type ControlAppearanceApi = {
712
925
  };
713
926
 
714
927
  /**
715
- * `lx.app.control.displayLanguage` — the preference behind that
928
+ * `lx.host.control.displayLanguage` — the preference behind that
716
929
  * language, for the one surface that edits it.
717
930
  */
718
931
  export type ControlDisplayLanguageApi = {
@@ -741,7 +954,7 @@ export type DeviceOrientationChangeEvent = {
741
954
  value: DeviceOrientation;
742
955
  };
743
956
 
744
- /** `lx.app.displayLanguage` — the language this lxapp renders in. */
957
+ /** `lx.host.displayLanguage` — the language this lxapp renders in. */
745
958
  export type DisplayLanguageApi = {
746
959
  /**
747
960
  * The language in effect right now, as a canonical BCP-47 tag. Map it to
@@ -781,7 +994,7 @@ export type DownloadOptionsBase = {
781
994
  */
782
995
  headers?: Record<string, string>;
783
996
  /** Request timeout in milliseconds. */
784
- timeout?: number;
997
+ timeoutMs?: number;
785
998
  /** Optional abort signal. */
786
999
  signal?: AbortSignal;
787
1000
  };
@@ -793,17 +1006,17 @@ export type DownloadsDownloadOptions = DownloadOptionsBase & {
793
1006
  * Optional filename hint for the system Downloads destination.
794
1007
  * This is not an app-owned `lx.fs` path.
795
1008
  */
796
- filePath?: string;
1009
+ suggestedName?: string;
1010
+ filePath?: never;
797
1011
  /** Save into the user's system Downloads directory. */
798
1012
  destination: 'downloads';
799
1013
  };
800
1014
 
801
1015
  export type DownloadsDownloadResult = {
802
- /** Native system Downloads path. Do not pass this to `lx.fs`. */
803
- filePath: SystemDownloadsPath;
804
- tempFilePath?: never;
1016
+ uri: SystemDownloadsPath;
1017
+ storage: 'downloads';
805
1018
  mimeType?: string;
806
- size: number;
1019
+ sizeBytes: number;
807
1020
  };
808
1021
 
809
1022
  /**
@@ -847,7 +1060,7 @@ export type ExtractVideoThumbnailResult = {
847
1060
  /**
848
1061
  * Generated thumbnail file path.
849
1062
  */
850
- tempFilePath: string;
1063
+ uri: string;
851
1064
  /**
852
1065
  * Output image width in pixels.
853
1066
  */
@@ -906,10 +1119,10 @@ export type GetImageInfoOptions = {
906
1119
 
907
1120
  /** Location APIs. */
908
1121
  export type GetLocationOptions = {
909
- type?: 'wgs84' | 'gcj02';
1122
+ coordinateSystem?: 'wgs84' | 'gcj02';
910
1123
  altitude?: boolean;
911
1124
  isHighAccuracy?: boolean;
912
- highAccuracyExpireTime?: number;
1125
+ timeoutMs?: number;
913
1126
  };
914
1127
 
915
1128
  export type GetVideoInfoOptions = {
@@ -949,7 +1162,7 @@ export type HostAppUpdateEvent = {
949
1162
  downloadedBytes?: number;
950
1163
  progress?: number;
951
1164
  } | {
952
- state: 'downloaded' | 'installRequested';
1165
+ state: 'downloaded' | 'installRequested' | 'storeOpened';
953
1166
  } | {
954
1167
  state: 'failed';
955
1168
  stage: HostAppUpdateApplyStage;
@@ -960,42 +1173,40 @@ export type HostAppUpdateInfo = {
960
1173
  version: string;
961
1174
  size?: number;
962
1175
  releaseNotes?: string[];
963
- isForceUpdate: boolean;
964
1176
  /**
965
- * Download and apply this checked update.
1177
+ * How this update is applied. `store` opens the platform marketplace;
1178
+ * `direct` downloads and self-installs. `lx.supports('app.selfUpdate')`
1179
+ * is true only for `direct`.
1180
+ */
1181
+ channel: 'direct' | 'store';
1182
+ /**
1183
+ * Apply this checked update.
966
1184
  *
967
- * `apply()` is single-use for this update object.
1185
+ * `apply()` is single-use for this update object. It also claims custom
1186
+ * host updates for the rest of this process, same as
1187
+ * {@link HostAppApi.claimCustomUpdate}.
968
1188
  *
969
- * The returned task can be awaited directly when progress is not needed, or
970
- * consumed with `for await...of` to render progress.
1189
+ * Await `task.result` when progress is not needed, or iterate
1190
+ * `task.progress` to render it.
971
1191
  *
972
- * Requires `lx.supports({ capability: 'selfUpdate' })`. Where the host cannot
973
- * install its own update it rejects with an unsupported-operation error;
974
- * use `version` and `releaseNotes` to guide users to the app marketplace.
1192
+ * On `direct`, downloads and hands off install. On `store`, opens the
1193
+ * platform store listing (no package is downloaded) and resolves
1194
+ * `storeOpened`; whether the user then updates is not reported.
975
1195
  */
976
1196
  apply(): HostAppUpdateTask;
977
1197
  };
978
1198
 
979
- export type HostAppUpdateIteratorResult = {
980
- done: boolean;
981
- value?: HostAppUpdateEvent;
982
- };
1199
+ export type HostAppUpdateIteratorResult = IteratorResult<HostAppUpdateEvent, void>;
983
1200
 
984
1201
  export type HostAppUpdateResult = {
985
- state: 'installRequested';
1202
+ /** `storeOpened` on a `store` channel: the listing opened, nothing was installed. */
1203
+ state: 'installRequested' | 'storeOpened';
986
1204
  };
987
1205
 
988
- export type HostAppUpdateTask = PromiseLike<HostAppUpdateResult> & AsyncIterable<HostAppUpdateEvent> & {
989
- next(): Promise<HostAppUpdateIteratorResult>;
990
- /** Stops iteration only. It does not cancel an app update already handed to the platform. */
991
- return(): Promise<HostAppUpdateIteratorResult>;
992
- catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<HostAppUpdateResult | TResult>;
993
- finally(onfinally?: (() => void) | null): Promise<HostAppUpdateResult>;
994
- wait(): Promise<HostAppUpdateResult>;
995
- };
1206
+ export type HostAppUpdateTask = Task<HostAppUpdateResult, HostAppUpdateEvent>;
996
1207
 
997
1208
  /**
998
- * Canonical platform-family label shared by `lx.app.getBaseInfo().os`
1209
+ * Canonical platform-family label shared by `lx.host.getBaseInfo().os`
999
1210
  * and `lx.getDeviceInfo().osName`. `"unknown"` is a non-product build.
1000
1211
  */
1001
1212
  export type HostOs = 'iOS' | 'macOS' | 'Android' | 'Windows' | 'Harmony' | 'unknown';
@@ -1007,6 +1218,43 @@ export type InstalledTerminalFont = {
1007
1218
  nerdIcons: boolean;
1008
1219
  };
1009
1220
 
1221
+ /**
1222
+ * Launch-at-startup control for the host app.
1223
+ * Absent (`undefined`) wherever the host cannot register a startup item.
1224
+ * `lx.supports('app.autostart')` and the member's presence always
1225
+ * agree, so either gate works:
1226
+ * ```ts
1227
+ * if (lx.supports('app.autostart')) {
1228
+ * // render the "Launch at startup" toggle
1229
+ * }
1230
+ * ```
1231
+ * Requires `capabilities.autostart: true` in `lingxia.yaml`; without it the
1232
+ * member is absent on all platforms. Declaring the capability never enables
1233
+ * autostart by itself — the SDK registers the app only when `setEnabled(true)`
1234
+ * is called, so the decision stays with the user (typically a settings-page
1235
+ * toggle, default off).
1236
+ * Host-app-level capability: like `checkUpdate` and `screenshot`, the methods
1237
+ * are available only to the native-assigned Control app; other lxapps receive
1238
+ * a permission error.
1239
+ * Local notifications as a Control-app resume affordance.
1240
+ * Absent unless the host declared `capabilities.notifications` and the
1241
+ * platform implements the local API. Presence and
1242
+ * `lx.supports('app.notification')` always agree.
1243
+ * Declaring the capability never prompts; permission runs on
1244
+ * `requestPermission()` or the first `show()` that reaches the OS.
1245
+ * Control app only. Guest lxapps receive a permission error.
1246
+ * A JSON value: what a navigation route parameter may hold. Not a way
1247
+ * to smuggle a payload — every route declares its own parameter
1248
+ * schema, and the framework caps size, count, and nesting.
1249
+ */
1250
+ export type JsonValue =
1251
+ | null
1252
+ | boolean
1253
+ | number
1254
+ | string
1255
+ | JsonValue[]
1256
+ | { [key: string]: JsonValue };
1257
+
1010
1258
  /**
1011
1259
  * Input event APIs.
1012
1260
  * Platform support: Android only
@@ -1030,38 +1278,8 @@ export type LxAppEnvVersion = 'release' | 'draft';
1030
1278
  /** LxApp metadata APIs. */
1031
1279
  export type LxAppReleaseType = 'release' | 'draft';
1032
1280
 
1033
- /** Boolean capability names accepted by `lx.supports`. */
1034
- export type LxCapabilityFlag = 'control' | 'terminal' | 'autostart' | 'notifications' | 'browser' | 'proxy' | 'selfUpdate' | 'process' | 'appUse' | 'computerUse' | 'browserUse' | 'mediaCapture';
1035
-
1036
- /**
1037
- * One capability question per call. The catalog is closed, so
1038
- * completion enumerates it and a typo is a type error. `capability`
1039
- * is the discriminant; only the `surface` branch accepts a `value`.
1040
- * Two surface answers describe an *affordance*, not whether the call
1041
- * succeeds: `tab` is "the host has an in-app browser" — without it a
1042
- * url still opens, in the OS browser instead — and `aside` is "a
1043
- * docked region exists right now", while a compact layout still opens
1044
- * the url through the in-app browser's own chrome. Ask them to decide
1045
- * what to render, not whether to call.
1046
- * `chrome` qualifies a window and only a window: it asks whether this
1047
- * host can produce that decoration, not merely a window.
1048
- */
1049
- export type LxCapabilityQuery = {
1050
- capability: 'surface';
1051
- value: 'window';
1052
- chrome?: WindowChrome;
1053
- } | {
1054
- capability: 'surface';
1055
- value: Exclude<LxSurfaceCapability, 'window'>;
1056
- } | {
1057
- capability: LxCapabilityFlag;
1058
- };
1059
-
1060
1281
  export type LxEnv = globalThis.LxEnv;
1061
1282
 
1062
- /** Surface placements accepted by `lx.supports`. */
1063
- export type LxSurfaceCapability = 'main' | 'aside' | 'float' | 'window' | 'tab';
1064
-
1065
1283
  /** Device action APIs. */
1066
1284
  export type MakePhoneCallOptions = {
1067
1285
  phoneNumber: string;
@@ -1072,11 +1290,11 @@ export type MediaObjectFit = 'cover' | 'contain' | 'fill' | 'fit';
1072
1290
  export type MediaRotation = 0 | 90 | 180 | 270;
1073
1291
 
1074
1292
  /**
1075
- * Result of `lx.showModal`. `canceled: false` means the user confirmed;
1293
+ * Result of `lx.showModal`. `status: 'ok'` means the user confirmed;
1076
1294
  * there is no third resolved outcome. Presentation failures reject.
1077
1295
  */
1078
1296
  export type ModalResult = {
1079
- canceled: false;
1297
+ status: 'ok';
1080
1298
  } | CanceledResult;
1081
1299
 
1082
1300
  /**
@@ -1140,6 +1358,24 @@ export type NavigationBarStylePatch = {
1140
1358
  dividerColor?: string | null;
1141
1359
  };
1142
1360
 
1361
+ /**
1362
+ * Where a notification tap, a menu item, or the tray goes.
1363
+ * `page` and `app` are the same contract as `lx.navigateTo` /
1364
+ * `lx.navigateToApp`: a configured page name and a query, ordinary
1365
+ * scene. `route` is a host-registered location that is not a page.
1366
+ * `appLink` is an `https://` product URL that is also a real inbound
1367
+ * App Link (`scene === 8003`). `activate` just brings the product
1368
+ * forward.
1369
+ * A branch carries its own fields and no others: a mixed target is a
1370
+ * parameter error, not a best guess.
1371
+ */
1372
+ export type NavigationTarget =
1373
+ | { kind: 'activate' }
1374
+ | { kind: 'page'; page: ConfiguredPageName; query?: PageQuery }
1375
+ | { kind: 'app'; appId: string; page?: ExternalPageName; query?: PageQuery }
1376
+ | { kind: 'route'; name: string; params?: Record<string, JsonValue> }
1377
+ | { kind: 'appLink'; url: string };
1378
+
1143
1379
  export type NetworkChangeCallback = (info: NetworkInfo) => void;
1144
1380
 
1145
1381
  export type NetworkInfo = {
@@ -1152,6 +1388,66 @@ export type NetworkInfo = {
1152
1388
  /** Network status APIs. */
1153
1389
  export type NetworkType = 'none' | 'unknown' | 'wifi' | '2g' | '3g' | '4g' | '5g' | 'ethernet';
1154
1390
 
1391
+ export type NotificationApi = {
1392
+ /**
1393
+ * Read the current permission without prompting. `'default'` means the
1394
+ * user has not been asked yet.
1395
+ */
1396
+ getPermission(): Promise<'granted' | 'denied' | 'default'>;
1397
+ /**
1398
+ * Ask for notification permission. Prompts where the OS has a prompt and
1399
+ * the user has not answered; otherwise reports the current setting.
1400
+ * Rejects when the prompt is left unanswered.
1401
+ */
1402
+ requestPermission(): Promise<'granted' | 'denied'>;
1403
+ /**
1404
+ * Post or replace a local notification. `id` is the replace key: anything
1405
+ * pending or delivered under it is replaced, whatever `status` comes back,
1406
+ * and its old tap target stops resolving. Omit `id` to get a generated
1407
+ * one. Omit `schedule`, or pass a time that is not in the future, to post
1408
+ * now.
1409
+ *
1410
+ * `target` is where the tap goes, and an omitted one means
1411
+ * `{ kind: 'activate' }` — bring the product forward, nothing else.
1412
+ * `{ kind: 'page' }` opens a page of this Control app the way
1413
+ * `lx.navigateTo` does. `{ kind: 'app' }` opens another lxapp the way
1414
+ * `lx.navigateToApp` does. `{ kind: 'route' }` names a location the host
1415
+ * registered at startup that is not a page. `{ kind: 'appLink' }` takes
1416
+ * an `https://` URL that is also a real inbound App Link
1417
+ * (`scene === 8003`). An unknown page or route, a parameter the route
1418
+ * did not declare, or a host that is not configured rejects here, before
1419
+ * anything is posted.
1420
+ *
1421
+ * `status` says what happened: `'posted'` — the OS accepted it for
1422
+ * display now, which is not a receipt that anyone saw or read it;
1423
+ * `'scheduled'` — queued with the OS for `schedule`; `'suppressed'` — an
1424
+ * immediate post while the product is already frontmost, where nothing is
1425
+ * posted and no permission is needed. A scheduled notification is
1426
+ * presented even if the product is frontmost when it fires.
1427
+ *
1428
+ * A tap resolves through the host, so a target that is gone by then — a
1429
+ * route the build no longer registers, a cancelled or replaced
1430
+ * notification, cleared app data — brings the product forward and says it
1431
+ * is unavailable rather than opening something else.
1432
+ */
1433
+ show(options: {
1434
+ id?: string;
1435
+ title: string;
1436
+ body?: string;
1437
+ target?: NavigationTarget;
1438
+ schedule?: { at: number } | { delayMs: number };
1439
+ /** No sound. The banner still appears. */
1440
+ silent?: boolean;
1441
+ }): Promise<{ id: string; status: 'posted' | 'scheduled' | 'suppressed' }>;
1442
+ /**
1443
+ * Remove what is pending or delivered under `id`, and retire its tap
1444
+ * target. Unknown ids are fine.
1445
+ */
1446
+ cancel(id: string): Promise<void>;
1447
+ /** Remove every local notification this API posted or scheduled. */
1448
+ cancelAll(): Promise<void>;
1449
+ };
1450
+
1155
1451
  /** File system APIs. */
1156
1452
  export type OpenFileOptions = {
1157
1453
  /** Local file path or runtime-managed temp path. */
@@ -1206,7 +1502,7 @@ export type OpenPageShared = {
1206
1502
  size?: OverlaySurfaceSize;
1207
1503
  interaction?: SurfaceInteraction;
1208
1504
  query?: PageQuery;
1209
- /** Caller-owned identity, for `lx.surface.get(key)` later. */
1505
+ /** Caller-owned identity, for `lx.surface.getByKey(key)` later. */
1210
1506
  key?: string;
1211
1507
  };
1212
1508
 
@@ -1219,7 +1515,7 @@ export type OpenUrlOptions = {
1219
1515
  /** Preferred docking side when the realized placement is an aside. */
1220
1516
  edge?: SurfaceEdge;
1221
1517
  size?: OverlaySurfaceSize;
1222
- /** Stable identity for `lx.surface.get(key)`. */
1518
+ /** Stable identity for `lx.surface.getByKey(key)`. */
1223
1519
  key?: string;
1224
1520
  };
1225
1521
 
@@ -1267,6 +1563,10 @@ export type PageTargetOptions = {
1267
1563
  query?: PageQuery;
1268
1564
  };
1269
1565
 
1566
+ export type PickFileOptions = Omit<ChooseFileOptions, 'multiple'>;
1567
+
1568
+ export type PickFileResult = { status: 'ok'; uri: string } | CanceledResult;
1569
+
1270
1570
  export type PreviewMediaAdvance = 'manual' | 'next' | 'loop';
1271
1571
 
1272
1572
  /** One change-stream event / the `current` snapshot. */
@@ -1278,28 +1578,12 @@ export type PreviewMediaChange = {
1278
1578
  export type PreviewMediaCloseReason = 'manual' | 'completed' | 'interrupted' | 'error';
1279
1579
 
1280
1580
  /**
1281
- * Handle returned synchronously from `lx.previewMedia(...)` — synchronous so
1282
- * listeners can be attached before the first event fires:
1283
- * - `presented` resolves once the first pixel of the underlying media has
1284
- * been composited to screen. Use this to time the hide of an overlay
1285
- * surface above the preview so the swap is seamless. Never rejects;
1286
- * resolves with no value when the first frame is up. Safe to ignore.
1287
- * - `current` is a live `{ index, source }` snapshot of the item on screen,
1288
- * updated as the user swipes and as the session auto-advances.
1289
- * - `onChange(listener)` fires for every item change. Returns an
1290
- * unsubscribe function.
1291
- * - `completed` resolves `{ reason, index, source }` when the preview
1292
- * session ends (manual / auto / interrupted / error), or rejects on abort.
1293
- * If the call was aborted before any frame was presented, `presented` still
1294
- * resolves (with no value) once the abort takes effect — it never rejects,
1295
- * to keep fire-and-forget usage safe.
1296
- * @example
1297
- * const preview = lx.previewMedia({ sources, startIndex: 2 });
1298
- * preview.onChange(({ source }) => markAsViewed(source.path));
1299
- * const { reason, source } = await preview.completed;
1581
+ * A media session. `presented` distinguishes a rendered first frame from
1582
+ * an early close, cancellation, or failure. `completed` reports closure;
1583
+ * native playback errors may report reason `error`, while request failures reject.
1300
1584
  */
1301
1585
  export type PreviewMediaHandle = {
1302
- readonly presented: Promise<void>;
1586
+ readonly presented: Promise<{ status: 'presented' } | { status: 'notPresented'; reason: 'canceled' | 'failed' | 'closed' }>;
1303
1587
  readonly current: PreviewMediaChange;
1304
1588
  onChange(listener: (change: PreviewMediaChange) => void): () => void;
1305
1589
  readonly completed: Promise<PreviewMediaResult>;
@@ -1443,15 +1727,42 @@ export type ScanCodeOptions = {
1443
1727
  };
1444
1728
 
1445
1729
  /**
1446
- * Result of `lx.scanCode`. Branch on `canceled` before reading the scan
1730
+ * Result of `lx.scanCode`. Branch on `status` before reading the scan
1447
1731
  * payload.
1448
1732
  */
1449
1733
  export type ScanCodeResult = {
1450
- canceled: false;
1734
+ status: 'ok';
1451
1735
  scanResult: string;
1452
1736
  scanType: string;
1453
1737
  } | CanceledResult;
1454
1738
 
1739
+ /**
1740
+ * Where `lx.host.setBadge` paints.
1741
+ * `auto` (the default) marks every product-owned surface this platform
1742
+ * has: the dock and the menu-bar item on macOS, the taskbar and the
1743
+ * notification-area item on Windows, the home-screen icon on iOS and
1744
+ * HarmonyOS. Name one only when that surface is the point.
1745
+ * Asynchronous because it reports what actually happened: a platform that
1746
+ * answers through its own callback has to be waited for to be believed.
1747
+ * A surface with nothing to paint on is reported, not raised: a macOS
1748
+ * status item exists from the moment a tray is declared but stays hidden
1749
+ * until `lx.tray.show()`, and a badge on a hidden item is not a badge
1750
+ * anyone can see. That resolves `false` whether you named the surface or
1751
+ * took `auto`; only a malfunction rejects.
1752
+ * Apple ties the badge to notification permission. On macOS the label
1753
+ * always reaches the system, but the Dock declines to draw it for an app
1754
+ * that is registered with Notification Center and not allowed — so a host
1755
+ * that declares `capabilities.notifications` and never got a yes resolves
1756
+ * `false` here. A host that never asks is unaffected.
1757
+ * On iOS the home-screen badge is drawn by the notification system, so
1758
+ * it needs notification permission and only accepts a number — that is
1759
+ * the OS's rule, not an API coupling. Android has no cross-vendor
1760
+ * launcher badge at all, so `setBadge` returns `false` there.
1761
+ */
1762
+ export type SetBadgeOptions = {
1763
+ surface?: 'auto' | 'appIcon' | 'tray';
1764
+ };
1765
+
1455
1766
  /** Share images, PDFs, or other files. */
1456
1767
  export type ShareFilesOptions = ShareTitleOptions & {
1457
1768
  /**
@@ -1586,7 +1897,7 @@ export type ShellOpenAppOptions = {
1586
1897
  */
1587
1898
  channel?: LxAppEnvVersion;
1588
1899
  targetVersion?: string;
1589
- /** Stable identity for `lx.surface.get(key)`. */
1900
+ /** Stable identity for `lx.surface.getByKey(key)`. */
1590
1901
  key?: string;
1591
1902
  };
1592
1903
 
@@ -1599,7 +1910,7 @@ export type ShellOpenAppOptions = {
1599
1910
  */
1600
1911
  export type ShellOpenDeclaredOptions = {
1601
1912
  /**
1602
- * Caller-owned identity, for `lx.surface.get(key)` later — the same key
1913
+ * Caller-owned identity, for `lx.surface.getByKey(key)` later — the same key
1603
1914
  * every opener takes. It carries one extra power here: a declaration can
1604
1915
  * be opened more than once, and the key is which instance you mean, so a
1605
1916
  * new key creates one. 1 to 128 UTF-8 bytes. Declarations without
@@ -1699,7 +2010,7 @@ export type ShellSurfacePatch = {
1699
2010
  };
1700
2011
 
1701
2012
  export type ShowActionSheetOptions = {
1702
- itemList: string[];
2013
+ items: readonly { id: string; label: string }[];
1703
2014
  itemColor?: string;
1704
2015
  };
1705
2016
 
@@ -1718,7 +2029,7 @@ export type ShowToastOptions = {
1718
2029
  title: string;
1719
2030
  icon?: 'success' | 'error' | 'loading' | 'none';
1720
2031
  image?: string;
1721
- duration?: number;
2032
+ durationMs?: number;
1722
2033
  mask?: boolean;
1723
2034
  position?: 'top' | 'center' | 'bottom';
1724
2035
  };
@@ -1736,7 +2047,7 @@ export type Storage = {
1736
2047
  * shape, exactly like a `JSON.parse` boundary; a missing key resolves
1737
2048
  * `undefined`, which a stored `null` never does.
1738
2049
  */
1739
- get<T = unknown>(key: string): Promise<T | undefined>;
2050
+ get<T = unknown>(key: string, decode?: (value: unknown) => T): Promise<T | undefined>;
1740
2051
  set(key: string, value: unknown): Promise<void>;
1741
2052
  /**
1742
2053
  * Resolves whether an exact key exists, without reading its value. Prefer
@@ -1761,7 +2072,7 @@ export type StorageInfo = {
1761
2072
  export type StreamSourceOptions = {
1762
2073
  provider: string;
1763
2074
  isLive: boolean;
1764
- duration?: number;
2075
+ durationSeconds?: number;
1765
2076
  params?: Record<string, unknown>;
1766
2077
  };
1767
2078
 
@@ -1781,19 +2092,16 @@ export type SurfaceApi = {
1781
2092
  */
1782
2093
  openDeclared(id: string): Promise<DeclaredSurface>;
1783
2094
  /**
1784
- * The live handle for a surface this lxapp opened **with a `key`**, found
1785
- * by that key or by its `id`. Removes the need to cache handles in order
1786
- * to reuse or close them. A surface opened without a `key` is not
1787
- * addressable — nothing else refers to a runtime-assigned id, so nothing
1788
- * registers it. A key you chose wins over an id it happens to spell.
2095
+ * Find a live surface by the explicit key passed when opening it.
2096
+ * Runtime-assigned ids are not lookup keys.
1789
2097
  */
1790
- get(keyOrId: string): AnySurface | undefined;
2098
+ getByKey(key: string): AnySurface | undefined;
1791
2099
  /**
1792
2100
  * Observe this presentation's viewport. Invoked immediately with the
1793
2101
  * current context, then again whenever it changes. Returns an unsubscribe
1794
2102
  * function.
1795
2103
  */
1796
- onContext(handler: (context: SurfaceContext) => void): () => void;
2104
+ watchContext(handler: (context: SurfaceContext) => void): () => void;
1797
2105
  };
1798
2106
 
1799
2107
  /** What every surface handle carries, whatever opened it. */
@@ -1842,12 +2150,15 @@ export type SurfaceClosedEvent = {
1842
2150
  };
1843
2151
 
1844
2152
  /**
1845
- * The current surface viewport context, delivered to `lx.surface.onContext()`
1846
- * so an lxapp can self-adapt (e.g. switch column count by `sizeClass`).
2153
+ * The current surface viewport context, delivered to `lx.surface.watchContext()`
2154
+ * so an lxapp can choose a compact or workspace View. Column count and
2155
+ * spacing inside `regular` use CSS or the raw `width` / `height`.
1847
2156
  */
1848
2157
  export type SurfaceContext = {
1849
- /** compact (<600) / medium (600–840) / expanded (>840), with hysteresis. */
1850
- sizeClass: 'compact' | 'medium' | 'expanded';
2158
+ /** Whether the host layout currently offers a docked aside. */
2159
+ aside: boolean;
2160
+ /** compact (<600) / regular (≥600). Shell medium/expanded are not distinct here. */
2161
+ sizeClass: 'compact' | 'regular';
1851
2162
  /** Actual surface viewport width in logical pixels. */
1852
2163
  width: number;
1853
2164
  /** Actual surface viewport height in logical pixels. */
@@ -1994,34 +2305,31 @@ export type TabBarItemPatch = {
1994
2305
  redDot?: boolean;
1995
2306
  };
1996
2307
 
2308
+ /**
2309
+ * Patch for `lx.tabBar.update()`. Items, badges, red dots, and
2310
+ * visibility only — a `style` field is rejected. Colors stay in
2311
+ * static `lxapp.json` `tabBar.style`. `backgroundColor` is
2312
+ * mobile-only; the desktop sidebar follows the host
2313
+ * `lingxia.yaml` theme.
2314
+ */
1997
2315
  export type TabBarPatch = {
1998
2316
  visibility?: TabBarVisibilityPreference;
1999
- style?: TabBarStylePatch | null;
2000
2317
  items?: readonly TabBarItemPatch[];
2001
2318
  };
2002
2319
 
2003
- export type TabBarStylePatch = {
2004
- foregroundColor?: string | null;
2005
- selectedForegroundColor?: string | null;
2006
- };
2007
-
2008
2320
  export type TabBarVisibilityPreference = 'auto' | 'visible' | 'hidden';
2009
2321
 
2010
2322
  /** External content in the in-app browser. */
2011
- export type TabSurface = SurfaceBase & {
2323
+ export type TabSurface = (SurfaceBase & {
2012
2324
  readonly kind: 'tab';
2013
2325
  readonly realized: 'tab' | 'aside';
2014
- /**
2015
- * `tab` when this handle owns exactly the tab it opened, and `close()` /
2016
- * `activate()` act on it. `group` when the browser chrome owns the tab
2017
- * strip: the content is open, but control belongs to that chrome, so both
2018
- * methods reject with `unsupported_placement`. Branch on this rather than
2019
- * on the old platform-dependent `null`.
2020
- */
2021
- readonly scope: 'tab' | 'group';
2022
- /** Bring this tab to the front of its browser. `scope: 'group'` rejects. */
2326
+ readonly scope: 'tab';
2023
2327
  activate(): Promise<void>;
2024
- };
2328
+ }) | (Omit<SurfaceBase, 'close' | 'onClose'> & {
2329
+ readonly kind: 'tab';
2330
+ readonly realized: 'tab' | 'aside';
2331
+ readonly scope: 'group';
2332
+ });
2025
2333
 
2026
2334
  export type TerminalApi = {
2027
2335
  /** Saved terminal settings, revision-checked on write. */
@@ -2157,6 +2465,11 @@ export type TerminalThemeSettings = {
2157
2465
  dark: string;
2158
2466
  };
2159
2467
 
2468
+ export type ToastHandle = {
2469
+ /** Dismiss this toast only; harmless after a newer toast replaces it. */
2470
+ dismiss(): Promise<void>;
2471
+ };
2472
+
2160
2473
  export type TrayApi = globalThis.TrayApi;
2161
2474
 
2162
2475
  /**
@@ -2164,9 +2477,10 @@ export type TrayApi = globalThis.TrayApi;
2164
2477
  * The tray is declared in `lingxia.yaml` (`tray:`); these update its dynamic
2165
2478
  * content at runtime.
2166
2479
  * **Desktop only.** Mobile platforms have no tray, so every method here is a
2167
- * no-op there (it never throws) — safe to call from portable code. For an
2168
- * app-icon badge that *is* cross-platform (including mobile), use
2169
- * `lx.app.setBadge`.
2480
+ * no-op there (it never throws) — safe to call from portable code.
2481
+ * The tray belongs to the product, not to the lxapp that happens to be
2482
+ * running, so these are Control-app only: a guest lxapp calling one receives
2483
+ * a permission error.
2170
2484
  */
2171
2485
  export type TrayMenuItem = {
2172
2486
  label: string;
@@ -2187,7 +2501,10 @@ export type UpdateFailedInfo = UpdateReadyInfo & {
2187
2501
  /**
2188
2502
  * Callback-based updates for this lxapp's bundle. Available to every
2189
2503
  * lxapp. To update the native host app, the Control app uses the
2190
- * task-based `lx.app.checkUpdate()` API instead.
2504
+ * task-based `lx.host.checkUpdate()` API instead.
2505
+ * Listeners are a set: later subscriptions do not replace earlier ones.
2506
+ * The last pending ready/failed event is replayed to each new
2507
+ * subscriber until a newer event replaces it.
2191
2508
  */
2192
2509
  export type UpdateManager = {
2193
2510
  applyUpdate(): void;
@@ -2199,14 +2516,10 @@ export type UpdateManager = {
2199
2516
 
2200
2517
  export type UpdateReadyInfo = {
2201
2518
  version?: string;
2202
- isForceUpdate?: boolean;
2203
2519
  channel?: "release" | "draft" | string;
2204
2520
  };
2205
2521
 
2206
- export type UploadIteratorResult = {
2207
- done: boolean;
2208
- value?: UploadProgressEvent;
2209
- };
2522
+ export type UploadIteratorResult = IteratorResult<UploadProgressEvent, void>;
2210
2523
 
2211
2524
  /**
2212
2525
  * Upload options. The file streams from disk, so the size ceiling is
@@ -2216,12 +2529,12 @@ export type UploadIteratorResult = {
2216
2529
  * - `multipart` (default) wraps the file in a `multipart/form-data`
2217
2530
  * envelope beside the `formData` text fields — what an ordinary form
2218
2531
  * endpoint parses. `name`, `fileName`, and `formData` describe that
2219
- * envelope.
2532
+ * envelope. `formData`, when present, must contain at least one field.
2220
2533
  * - `raw` sends the file bytes as the entire body. Presigned
2221
2534
  * object-storage URLs (S3, OSS, Azure Blob) need this: a multipart
2222
2535
  * envelope would be stored verbatim as the object's contents,
2223
- * boundary lines and all. `name` and `formData` are then rejected
2224
- * rather than silently dropped, and `fileName` is ignored.
2536
+ * boundary lines and all. `name`, `formData`, and `fileName` are
2537
+ * then rejected rather than silently dropped.
2225
2538
  * @example
2226
2539
  * ```ts
2227
2540
  * // A presigned URL is signed for one method and one Content-Type,
@@ -2233,8 +2546,8 @@ export type UploadIteratorResult = {
2233
2546
  * bodyMode: 'raw',
2234
2547
  * mimeType: 'video/mp4',
2235
2548
  * });
2236
- * for await (const event of task) render(event.progress);
2237
- * const { statusCode } = await task;
2549
+ * for await (const event of task.progress) render(event.progress);
2550
+ * const { statusCode } = await task.result;
2238
2551
  * ```
2239
2552
  */
2240
2553
  export type UploadOptions = {
@@ -2247,14 +2560,6 @@ export type UploadOptions = {
2247
2560
  * A presigned URL is signed for exactly one method, usually `PUT`.
2248
2561
  */
2249
2562
  method?: 'POST' | 'PUT' | 'PATCH';
2250
- /**
2251
- * How the file bytes are framed. Default: `multipart`.
2252
- * `raw` sends them as the whole body under a `Content-Length` taken from
2253
- * the file itself, which is what presigned endpoints require.
2254
- */
2255
- bodyMode?: 'multipart' | 'raw';
2256
- /** Name of the multipart part carrying the file. Default: `file`. Multipart only. */
2257
- name?: string;
2258
2563
  /**
2259
2564
  * Optional request headers.
2260
2565
  * Restricted headers such as `Referer` are ignored by the runtime.
@@ -2263,12 +2568,8 @@ export type UploadOptions = {
2263
2568
  * carries the part boundary.
2264
2569
  */
2265
2570
  headers?: Record<string, string>;
2266
- /** Text fields sent alongside the file in the envelope. Multipart only. */
2267
- formData?: Record<string, string>;
2268
2571
  /** Request timeout in milliseconds. */
2269
- timeout?: number;
2270
- /** Filename announced for the file part. Defaults to the file's own name. Multipart only. */
2271
- fileName?: string;
2572
+ timeoutMs?: number;
2272
2573
  /**
2273
2574
  * File MIME type. Types the file part under `multipart`; becomes the
2274
2575
  * request `Content-Type` under `raw`, where it defaults to
@@ -2277,11 +2578,30 @@ export type UploadOptions = {
2277
2578
  mimeType?: string;
2278
2579
  /** Optional abort signal. */
2279
2580
  signal?: AbortSignal;
2280
- };
2581
+ } & (
2582
+ | {
2583
+ /**
2584
+ * How the file bytes are framed. Default: `multipart`.
2585
+ */
2586
+ bodyMode?: 'multipart';
2587
+ /** Name of the multipart part carrying the file. Default: `file`. */
2588
+ name?: string;
2589
+ /** Text fields sent alongside the file. Must be non-empty when set. */
2590
+ formData?: Record<string, string>;
2591
+ /** Filename announced for the file part. Defaults to the file's own name. */
2592
+ fileName?: string;
2593
+ }
2594
+ | {
2595
+ /** Send the file bytes as the whole body. Multipart fields are rejected. */
2596
+ bodyMode: 'raw';
2597
+ name?: never;
2598
+ formData?: never;
2599
+ fileName?: never;
2600
+ }
2601
+ );
2281
2602
 
2282
2603
  export type UploadProgressEvent = {
2283
- /** `completed` and `canceled` are terminal; iteration ends after either. */
2284
- kind: 'progress' | 'canceled' | 'completed';
2604
+ kind: 'progress' | 'canceled';
2285
2605
  /** Bytes handed to the socket so far, envelope included under `multipart`. */
2286
2606
  uploadedBytes?: number;
2287
2607
  /**
@@ -2292,8 +2612,12 @@ export type UploadProgressEvent = {
2292
2612
  totalBytes?: number;
2293
2613
  /** `uploadedBytes / totalBytes`, absent while the total is unknown or zero. */
2294
2614
  progress?: number;
2295
- /** Present on `completed` only. */
2296
- result?: UploadResult;
2615
+ } | {
2616
+ kind: 'completed';
2617
+ uploadedBytes?: number;
2618
+ totalBytes?: number;
2619
+ progress?: number;
2620
+ result: UploadResult;
2297
2621
  };
2298
2622
 
2299
2623
  export type UploadResult = {
@@ -2303,15 +2627,7 @@ export type UploadResult = {
2303
2627
  data: string;
2304
2628
  };
2305
2629
 
2306
- export type UploadTask = PromiseLike<UploadResult> & AsyncIterable<UploadProgressEvent> & {
2307
- next(): Promise<UploadIteratorResult>;
2308
- /** Stops iteration only. Does not cancel the underlying upload task. */
2309
- return(): Promise<UploadIteratorResult>;
2310
- catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<UploadResult | TResult>;
2311
- finally(onfinally?: (() => void) | null): Promise<UploadResult>;
2312
- cancel(): Promise<void>;
2313
- wait(): Promise<UploadResult>;
2314
- };
2630
+ export type UploadTask = CancelableTask<UploadResult, UploadProgressEvent>;
2315
2631
 
2316
2632
  export type VideoCompressQuality = 'low' | 'medium' | 'high';
2317
2633
 
@@ -2319,7 +2635,7 @@ export type VideoContext = {
2319
2635
  play(): void;
2320
2636
  pause(): void;
2321
2637
  stop(): void;
2322
- seek(position: number): void;
2638
+ seek(positionSeconds: number): void;
2323
2639
  requestFullScreen(): void;
2324
2640
  exitFullScreen(): void;
2325
2641
  setStreamSource(options: StreamSourceOptions): void;
@@ -2436,7 +2752,7 @@ export type WindowsTerminalInlineImageStatus = {
2436
2752
  /**
2437
2753
  * Host app identity. Everything here is fixed for the life of the process;
2438
2754
  * the language the app renders in is not, and lives on
2439
- * `lx.app.displayLanguage`.
2755
+ * `lx.host.displayLanguage`.
2440
2756
  */
2441
2757
  export interface AppBaseInfo {
2442
2758
  /**
@@ -2446,7 +2762,7 @@ export interface AppBaseInfo {
2446
2762
  os: HostOs;
2447
2763
  productName: string;
2448
2764
  version: string;
2449
- SDKVersion: string;
2765
+ sdkVersion: string;
2450
2766
  }
2451
2767
 
2452
2768
  /** Device info APIs. */
@@ -2516,9 +2832,9 @@ export interface SystemSettingInfo {
2516
2832
  /** Wi-Fi APIs. */
2517
2833
  export interface WifiInfo {
2518
2834
  /** Service Set Identifier (network name) */
2519
- SSID: string;
2835
+ ssid: string;
2520
2836
  /** Basic Service Set Identifier (MAC address) */
2521
- BSSID?: string;
2837
+ bssid?: string;
2522
2838
  /** Whether the network is secure (requires password) */
2523
2839
  secure: boolean;
2524
2840
  /** Signal strength (0-100, higher is better) */
@@ -2527,16 +2843,14 @@ export interface WifiInfo {
2527
2843
  frequency?: number;
2528
2844
  }
2529
2845
 
2530
- export declare class DirEntry {
2531
- private constructor();
2846
+ export interface DirEntry {
2532
2847
  readonly name: string;
2533
2848
  readonly isFile: boolean;
2534
2849
  readonly isDirectory: boolean;
2535
2850
  readonly isSymlink: boolean;
2536
2851
  }
2537
2852
 
2538
- export declare class LxFile {
2539
- private constructor();
2853
+ export interface LxFile {
2540
2854
  /** The path supplied to `lx.fs.file`. */
2541
2855
  readonly path: string;
2542
2856
  /** Read the complete file as strict UTF-8 text. */
@@ -2560,6 +2874,57 @@ export declare class LxFile {
2560
2874
  stat(): Promise<FileStats>;
2561
2875
  }
2562
2876
 
2877
+ declare global {
2878
+ interface ClipboardApi {
2879
+ /**
2880
+ * Replace the clipboard with Unicode text.
2881
+ * Empty string is a valid payload (it is not `clear()`). The runtime does not
2882
+ * present a toast — call `lx.showToast` if the product wants one. Rejects
2883
+ * `E_INVALID_ARG` above 1 MiB.
2884
+ */
2885
+ writeText(text: string): Promise<void>;
2886
+ /**
2887
+ * Read Unicode text.
2888
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the OS paste
2889
+ * prompt (iOS 16+, macOS 15.4+). No text representation (empty clipboard, or
2890
+ * image-only) resolves `{ status: 'empty' }`. A copied empty
2891
+ * string resolves `{ status: 'ok', text: '' }`.
2892
+ * Rejects `E_PERMISSION_DENIED` when the host denies clipboard access
2893
+ * outright: a macOS "never allow" setting, or HarmonyOS without
2894
+ * `ohos.permission.READ_PASTEBOARD`. Android denies a read while the app has
2895
+ * no window focus and reports it as an empty clipboard, so read in response
2896
+ * to a user action.
2897
+ */
2898
+ readText(): Promise<ClipboardTextResult>;
2899
+ /**
2900
+ * Replace the clipboard with a typed item.
2901
+ * Rejects `E_INVALID_ARG` for text above 1 MiB or an image file that does not
2902
+ * decode.
2903
+ */
2904
+ write(item: ClipboardWriteItem): Promise<void>;
2905
+ /**
2906
+ * Read the clipboard.
2907
+ * Omit `type` to receive every representation this host can surface.
2908
+ * Pass `type` to request one; if that representation is absent, the
2909
+ * completed result is `{ status: 'empty' }` rather than a mismatch error.
2910
+ * Images arrive as a temporary PNG under `lx://temp`. Dismissal and
2911
+ * permission behave as in `readText`.
2912
+ */
2913
+ read(options?: ClipboardReadOptions): Promise<ClipboardReadResult>;
2914
+ /** Remove every representation. */
2915
+ clear(): Promise<void>;
2916
+ /**
2917
+ * Which representations are present, without reading payloads. An empty
2918
+ * array is an empty clipboard; representations this runtime cannot
2919
+ * round-trip (HTML, files) are omitted.
2920
+ * Never shows the OS paste prompt and needs no permission on any host, so it
2921
+ * is the way to decide whether to offer "Paste". The answer is a hint —
2922
+ * content may change before you read it.
2923
+ */
2924
+ types(): Promise<ClipboardType[]>;
2925
+ }
2926
+ }
2927
+
2563
2928
  declare global {
2564
2929
  interface FileSystemApi {
2565
2930
  /**
@@ -2567,45 +2932,54 @@ declare global {
2567
2932
  * Relative paths resolve under `lx.env.USER_DATA_PATH`. Creating a reference
2568
2933
  * does not require the path to exist.
2569
2934
  */
2570
- file(path: string): LxFile;
2935
+ file(path: ManagedPath): LxFile;
2571
2936
  /** Test whether a managed path currently exists. */
2572
- exists(path: string): Promise<boolean>;
2937
+ exists(path: ManagedPath): Promise<boolean>;
2573
2938
  /** Read metadata for a managed path. */
2574
- stat(path: string): Promise<FileStats>;
2939
+ stat(path: ManagedPath): Promise<FileStats>;
2575
2940
  /** The direct children of a managed directory. */
2576
- readDir(path: string): Promise<DirEntry[]>;
2941
+ readDir(path: ManagedPath): Promise<DirEntry[]>;
2577
2942
  /** Create a managed directory. */
2578
- mkdir(path: string, options?: FsMkdirOptions): Promise<void>;
2943
+ mkdir(path: ManagedPath, options?: FsMkdirOptions): Promise<void>;
2579
2944
  /** Write UTF-8 text or bytes to a managed file. */
2580
- write(path: string, data: string, options?: FsWriteOptions): Promise<void>;
2945
+ write(path: ManagedPath, data: string, options?: FsWriteOptions): Promise<void>;
2581
2946
  /** Copy a managed file. */
2582
- copy(source: string, destination: string, options?: FsCopyOptions): Promise<void>;
2947
+ copy(source: ManagedPath, destination: ManagedPath, options?: FsCopyOptions): Promise<void>;
2583
2948
  /** Rename or move a managed file or directory. */
2584
- rename(source: string, destination: string, options?: FsRenameOptions): Promise<void>;
2949
+ rename(source: ManagedPath, destination: ManagedPath, options?: FsRenameOptions): Promise<void>;
2585
2950
  /** Remove a managed file or directory. */
2586
- remove(path: string, options?: FsRemoveOptions): Promise<void>;
2951
+ remove(path: ManagedPath, options?: FsRemoveOptions): Promise<void>;
2587
2952
  }
2588
2953
  }
2589
2954
 
2590
2955
  declare global {
2591
2956
  interface HostAppApi {
2592
2957
  /**
2593
- * `lx.app.screenshot(options?)` — capture the host app's window as a PNG.
2958
+ * `lx.host.screenshot(options?)` — capture the host app's window as a PNG.
2594
2959
  * App-level semantics, one level above any page/WebView capture: the image
2595
2960
  * is what the user sees of the whole app — host-drawn navigation chrome,
2596
2961
  * native overlays, and every composited WebView, not just this lxapp's web
2597
2962
  * content. Because that view can include other lxapps' UI, the API is
2598
- * restricted to the Control app, like the other host-level APIs on `lx.app`.
2963
+ * restricted to the Control app, like the other host-level APIs on `lx.host`.
2599
2964
  */
2600
2965
  screenshot(options?: AppScreenshotOptions): Promise<AppScreenshotResult>;
2601
2966
  /**
2602
- * Check whether the host app has an update.
2603
- * This host-level capability is restricted to the Control app. Calling it opts
2604
- * the process into custom update handling. Incompatible updates are hidden as
2605
- * `hasUpdate: false`; platforms that cannot apply a package may still return
2606
- * metadata and reject when `update.apply()` is invoked.
2967
+ * Query whether the host app has an update.
2968
+ * This host-level capability is restricted to the Control app. A successful
2969
+ * check does **not** take over the built-in auto-flow — that is
2970
+ * `claimCustomUpdate()` or `update.apply()`. Permission denial or a failed
2971
+ * check claims nothing. Incompatible updates are hidden as
2972
+ * `hasUpdate: false`. Store-channel hosts still surface a newer feed version;
2973
+ * `apply()` opens the store listing instead of downloading.
2607
2974
  */
2608
2975
  checkUpdate(): Promise<HostAppUpdateCheckResult>;
2976
+ /**
2977
+ * Claim the process-lifetime custom host-update flow.
2978
+ * Irreversible: the built-in auto-flow will not prompt or download again,
2979
+ * including after the calling page unloads. Does not cancel an already-started
2980
+ * update task. Later failed checks do not undo a claim already made.
2981
+ */
2982
+ claimCustomUpdate(): void;
2609
2983
  readonly env: HostAppEnv;
2610
2984
  /**
2611
2985
  * Read the host app's identity: OS, product name, product version, and SDK
@@ -2621,30 +2995,32 @@ declare global {
2621
2995
  */
2622
2996
  exit(): void;
2623
2997
  /**
2624
- * Set the app-icon badge, for example an unread count.
2625
- * This targets the dock on macOS, taskbar on Windows, and home/launcher icon
2626
- * on mobile — the product's own icon, not the calling lxapp's, so it is
2627
- * Control app only and other lxapps get a permission error. Null or an empty
2628
- * string clears it. Unsupported platforms treat the call as a no-op.
2629
- */
2630
- setBadge(value: string | number | null): void;
2998
+ * Mark the product in system chrome, for example with an unread count.
2999
+ * One call, because "where the count goes" is the platform's answer, not the
3000
+ * caller's: `auto` paints every product-owned surface this platform has — the
3001
+ * dock and the menu-bar item on macOS, the taskbar and the notification-area
3002
+ * item on Windows, the home-screen icon on iOS and HarmonyOS. Name a
3003
+ * `surface` only when one of them is the point.
3004
+ * It is the product's chrome, not the calling lxapp's, so it is Control app
3005
+ * only. Null or an empty string clears it.
3006
+ * Returns whether anything was actually painted. A platform with no such
3007
+ * chrome is a no-op that returns `false` rather than an error — portable code
3008
+ * can call this unconditionally.
3009
+ */
3010
+ setBadge(value: string | number | null, options?: SetBadgeOptions): Promise<boolean>;
2631
3011
  }
2632
3012
  }
2633
3013
 
2634
3014
  declare global {
2635
3015
  interface Lx {
2636
- readonly app: HostAppApi;
2637
- /**
2638
- * Whether this host exposes a capability to this Logic context, right now.
2639
- * Synchronous, because it is meant to be called from render paths. The answer
2640
- * is live and may be stale by the time you act on it — it is an affordance for
2641
- * deciding what to render, not a replacement for handling a rejection.
2642
- * `{ capability: 'surface', value: 'aside' }` in particular changes when a
2643
- * desktop window crosses the compact breakpoint; pair it with
2644
- * `lx.surface.onContext` instead of polling. The answer is per runtime context:
2645
- * a context that does not expose an API reports false for it.
2646
- */
2647
- supports(query: LxCapabilityQuery): boolean;
3016
+ readonly host: HostAppApi;
3017
+ /**
3018
+ * Frozen feature support, not permission or current layout. Unknown strings
3019
+ * return false; non-strings throw TypeError. Required features also need an
3020
+ * appropriate lxapp.json minRuntime.
3021
+ */
3022
+ supports(feature: LxFeature): boolean;
3023
+ readonly clipboard: ClipboardApi;
2648
3024
  /** Vibrate briefly, where the device has a vibrator. */
2649
3025
  vibrateShort(): boolean;
2650
3026
  /** Vibrate for a longer pulse, where the device has a vibrator. */
@@ -2691,8 +3067,8 @@ declare global {
2691
3067
  * `method: 'PUT'` with `bodyMode: 'raw'` to send the file bytes as the whole
2692
3068
  * body instead, which is what presigned object-storage URLs expect.
2693
3069
  * Returns the task handle synchronously, before the transfer starts, so
2694
- * progress and cancellation can be wired up without racing it: the handle is
2695
- * awaitable for the final result, async-iterable for progress, and cancelable.
3070
+ * progress and cancellation can be wired up without racing it: `result` settles
3071
+ * once, `progress` streams to one consumer, and `cancel()` aborts.
2696
3072
  */
2697
3073
  uploadFile(options: UploadOptions): UploadTask;
2698
3074
  /**
@@ -2701,17 +3077,20 @@ declare global {
2701
3077
  * `mode: "auto"`.
2702
3078
  */
2703
3079
  openFile(options: OpenFileOptions): Promise<void>;
3080
+ /** Pick one file. A successful selection returns one opaque URI. */
3081
+ pickFile(options?: PickFileOptions): Promise<PickFileResult>;
3082
+ /** Pick one or more files. Dismissal is a normal outcome. */
3083
+ pickFiles(options?: PickFileOptions): Promise<ChooseFileResult>;
2704
3084
  /**
2705
- * Opens a file picker.
2706
- * Resolves `{ canceled: true }` only when the user dismisses the picker. A
2707
- * completed selection resolves `{ canceled: false, paths }` with at least one
3085
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the picker. A
3086
+ * completed selection resolves `{ status: 'ok', paths }` with at least one
2708
3087
  * path. Rejects when the picker fails or returns an invalid payload.
2709
3088
  */
2710
- chooseFile(options?: ChooseFileOptions): Promise<ChooseFileResult>;
3089
+ chooseFile(options: never): Promise<never>;
2711
3090
  /**
2712
3091
  * Opens a directory picker.
2713
- * Resolves `{ canceled: true }` only when the user dismisses the picker. A
2714
- * completed selection resolves `{ canceled: false, path }`. Rejects when the
3092
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the picker. A
3093
+ * completed selection resolves `{ status: 'ok', path }`. Rejects when the
2715
3094
  * picker fails or returns an invalid payload.
2716
3095
  */
2717
3096
  chooseDirectory(options?: ChooseDirectoryOptions): Promise<ChooseDirectoryResult>;
@@ -2730,8 +3109,8 @@ declare global {
2730
3109
  compressImage(options: CompressImageOptions): Promise<CompressImageResult>;
2731
3110
  /**
2732
3111
  * Opens the media picker or camera.
2733
- * Resolves `{ canceled: true }` only when the user dismisses the picker. A
2734
- * completed selection resolves `{ canceled: false, entries }` with at least one
3112
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the picker. A
3113
+ * completed selection resolves `{ status: 'ok', entries }` with at least one
2735
3114
  * entry. Rejects when capture or selection fails, or the host returns an invalid
2736
3115
  * payload.
2737
3116
  */
@@ -2739,10 +3118,8 @@ declare global {
2739
3118
  /**
2740
3119
  * Synchronously returns a JS handle so listeners can be attached before the
2741
3120
  * first event fires:
2742
- * - `presented`: Promise, resolves with no value when the first pixel of the
2743
- * underlying media is composited to screen. Also resolves unconditionally
2744
- * once `completed` settles, so consumers can safely ignore it (it never
2745
- * rejects).
3121
+ * - `presented`: resolves `{ status: 'presented' }` only after the first
3122
+ * frame; otherwise `{ status: 'notPresented', reason }`. Never rejects.
2746
3123
  * - `current`: `{ index, source }` snapshot of the item on screen, updated
2747
3124
  * live as the user swipes / the session auto-advances.
2748
3125
  * - `onChange(listener)`: fires `{ index, source }` for every item change
@@ -2760,8 +3137,8 @@ declare global {
2760
3137
  saveVideoToPhotosAlbum(options: SaveMediaOptions): Promise<void>;
2761
3138
  /**
2762
3139
  * Opens the scanner.
2763
- * Resolves `{ canceled: true }` only when the user dismisses the scanner. A
2764
- * completed scan resolves `{ canceled: false, scanResult, scanType }`. Rejects
3140
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the scanner. A
3141
+ * completed scan resolves `{ status: 'ok', scanResult, scanType }`. Rejects
2765
3142
  * when scanning fails or the host returns an invalid payload.
2766
3143
  */
2767
3144
  scanCode(options?: ScanCodeOptions): Promise<ScanCodeResult>;
@@ -2811,15 +3188,18 @@ declare global {
2811
3188
  getSystemSetting(): SystemSettingInfo;
2812
3189
  /**
2813
3190
  * Shows a list of actions.
2814
- * Resolves `{ canceled: false, index }` when the user selects an item; `index`
2815
- * points into `options.itemList`. Resolves `{ canceled: true }` only when the
3191
+ * Resolves `{ status: 'ok', id }` when the user selects an item; `id` identifies the selected item. Resolves `{ status: 'canceled' }` only when the
2816
3192
  * user dismisses the sheet. Rejects when presentation fails or the host returns
2817
3193
  * an invalid selection.
2818
3194
  */
2819
3195
  showActionSheet(options: ShowActionSheetOptions): Promise<ActionSheetResult>;
3196
+ /** Acknowledgement-only dialog. Resolves when the user dismisses it. */
3197
+ alert(options: AlertOptions): Promise<void>;
3198
+ /** Ask a yes/no question. Dismissal resolves false; presentation failure rejects. */
3199
+ confirm(options: ConfirmOptions): Promise<boolean>;
2820
3200
  /**
2821
3201
  * Shows a confirmation modal.
2822
- * Resolves `{ canceled: false }` when the user confirms and `{ canceled: true }`
3202
+ * Resolves `{ status: 'ok' }` when the user confirms and `{ status: 'canceled' }`
2823
3203
  * only when the user dismisses or cancels the modal. Rejects when presentation
2824
3204
  * fails or the host returns an invalid payload.
2825
3205
  */
@@ -2876,15 +3256,19 @@ declare global {
2876
3256
  reLaunch(options: ReLaunchOptions): Promise<void>;
2877
3257
  readonly shell: ShellApi;
2878
3258
  readonly tabBar: TabBarApi;
2879
- /** Show toast function */
2880
- showToast(options: ShowToastOptions): Promise<void>;
2881
- /** Hide toast function */
3259
+ /**
3260
+ * Presents a toast and resolves a handle once the host accepted it. The handle
3261
+ * dismisses only this toast, never a newer one — including a newer one posted
3262
+ * by another lxapp onto a host's shared overlay.
3263
+ */
3264
+ showToast(options: ShowToastOptions): Promise<ToastHandle>;
3265
+ /** Hides whichever toast is showing. */
2882
3266
  hideToast(): Promise<void>;
2883
3267
  readonly tray: TrayApi;
2884
3268
  /**
2885
3269
  * Return the callback-based update manager for this lxapp's bundle. This is
2886
3270
  * available to every lxapp and is distinct from the Control-app-only
2887
- * `lx.app.checkUpdate()`, which updates the native host app.
3271
+ * `lx.host.checkUpdate()`, which updates the native host app.
2888
3272
  */
2889
3273
  getUpdateManager(): UpdateManager;
2890
3274
  }
@@ -2978,36 +3362,37 @@ declare global {
2978
3362
  * `lingxia.yaml`, opened with the declaration's own presentation.
2979
3363
  */
2980
3364
  openDeclared(id: string): Promise<DeclaredSurface>;
3365
+ /** Find a live surface by its explicit caller-owned key. Runtime ids are not keys. */
3366
+ getByKey(key: string): AnySurface | undefined;
2981
3367
  /**
2982
- * `lx.surface.get(keyOrId)` — the live handle for a surface this lxapp opened
2983
- * **with a `key`**, so no caller has to cache one in order to reuse or close
2984
- * it. An unkeyed surface is not addressable: nothing registers it, because
2985
- * holding one for the session costs its closures and its message port and
2986
- * nobody can look up a uuid they never chose.
2987
- * A `key` you chose wins over a runtime-assigned `id`, so a key that happens
2988
- * to spell another surface's id still finds yours.
2989
- */
2990
- get(keyOrId: string): AnySurface | undefined;
2991
- /**
2992
- * `lx.surface.onContext(handler)` — register a JS callback (scoped to this
3368
+ * `lx.surface.watchContext(handler)` — register a JS callback (scoped to this
2993
3369
  * lxapp's JS context), invoke it immediately, then again whenever that
2994
- * presentation's actual viewport changes. Returns an unsubscribe fn.
3370
+ * presentation's viewport or host docking availability changes. Returns an unsubscribe fn.
2995
3371
  */
2996
- onContext(handler: (context: SurfaceContext) => void): () => void;
3372
+ watchContext(handler: (context: SurfaceContext) => void): () => void;
2997
3373
  }
2998
3374
  }
2999
3375
 
3000
3376
  declare global {
3001
3377
  interface TabBarApi {
3002
- /** Patch this lxapp's tab bar; unset fields stay as they are. */
3378
+ /**
3379
+ * Patch this lxapp's tab bar; unset fields stay as they are.
3380
+ * Items, badges, red dots, and visibility only. A `style` field is
3381
+ * rejected — colors stay in static `lxapp.json` `tabBar.style`.
3382
+ * `tabBar.style.backgroundColor` is mobile-only: it paints the bar on
3383
+ * iOS / Android / Harmony. On macOS / Windows the sidebar follows the
3384
+ * host `lingxia.yaml` theme (`windowBackgroundColor`) instead, so a
3385
+ * `#FFFFFF` fill cannot paint a card on the sidebar. Other style keys
3386
+ * (`foregroundColor`, `selectedForegroundColor`, `dividerColor`) may
3387
+ * tint items while the desktop host is light; a dark host uses the
3388
+ * shell theme for every key.
3389
+ */
3003
3390
  update(patch: TabBarPatch): Promise<void>;
3004
3391
  }
3005
3392
  }
3006
3393
 
3007
3394
  declare global {
3008
3395
  interface TrayApi {
3009
- /** lx.tray.setBadge(value) — the menu-bar / system-tray badge. Null/empty clears it. */
3010
- setBadge(value: string | number | null): void;
3011
3396
  /** lx.tray.setIcon(icon) — replace the tray icon (a resource path). */
3012
3397
  setIcon(icon: string): void;
3013
3398
  /** lx.tray.setTitle(text) — text shown beside the icon (macOS). Empty clears it. */
@@ -3033,3 +3418,6 @@ declare global {
3033
3418
  }
3034
3419
 
3035
3420
  export {};
3421
+
3422
+ /** Feature contracts generated from the runtime registry. */
3423
+ export type LxFeature = 'app.appUse' | 'app.autostart' | 'app.banner' | 'app.browser' | 'app.browserUse' | 'app.computerUse' | 'app.mediaCapture' | 'app.notification' | 'app.proxy' | 'app.selfUpdate' | 'process' | 'surface.tab' | 'surface.window' | 'surface.window.fullChrome' | 'terminal';