@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
@@ -1,7 +1,18 @@
1
1
  declare const appDownloadPathBrand: unique symbol;
2
2
  declare const systemDownloadsPathBrand: unique symbol;
3
- export interface PageConfig<TData extends Record<string, unknown> = Record<string, unknown>> {
4
- data?: TData;
3
+ /** Managed paths and relative userdata paths; system Downloads references are excluded. */
4
+ export type ManagedPath = string & {
5
+ readonly [systemDownloadsPathBrand]?: never;
6
+ };
7
+ /** JSON-shaped state; undefined removes a property in setData. */
8
+ export type PageDataValue<T> = unknown extends T ? T : T extends string | number | boolean | null | undefined ? T : T extends (...args: never[]) => unknown ? never : T extends object ? {
9
+ [K in keyof T]: PageDataValue<T[K]>;
10
+ } : never;
11
+ export type NoReservedPageMembers<T> = {
12
+ [K in keyof T]: K extends 'data' ? T[K] : K extends keyof PageInstance | '_setData' | '_cancelPendingSetData' ? never : T[K];
13
+ };
14
+ export interface PageConfig<TData extends object = Record<string, unknown>> {
15
+ data?: TData & PageDataValue<TData>;
5
16
  onLoad?: (options?: PageLoadOptions) => void | Promise<void>;
6
17
  onShow?: () => void | Promise<void>;
7
18
  onReady?: () => void | Promise<void>;
@@ -31,25 +42,53 @@ export type NoLifecycleTypos<TCustom, TNames extends string> = {
31
42
  };
32
43
  /**
33
44
  * A `setData` key that addresses inside `data` — `'a.b'` or `'rows[0].name'`.
34
- * Values behind a path stay `unknown`: the runtime resolves the path, so the
35
- * type cannot.
45
+ * Kept for documentation; nested writes go through `setPath` (checked) or
46
+ * `setDataPath` (unchecked). `setData` no longer accepts these keys.
36
47
  */
37
48
  export type PageDataPath = `${string}.${string}` | `${string}[${number}]${string}`;
38
49
  /**
39
- * A field initialized to `null` or `[]` states nothing about what will fill it
40
- * later, so it stays open. Annotate it (`null as Profile | null`) to have the
41
- * fill checked.
50
+ * @deprecated Page `data` is no longer widened for `null` / `[]` initializers.
51
+ * Annotate the field (`null as Profile | null`) when a later fill should be
52
+ * checked.
42
53
  */
43
- export type LazyInitField<T> = [T] extends [null | undefined] ? unknown : [T] extends [never[]] ? unknown[] : T;
54
+ export type LazyInitField<T> = T;
55
+ type PageReadonlyDepth = [never, 0, 1, 2, 3, 4, 5, 6];
56
+ type PagePrimitive = string | number | boolean | bigint | symbol | null | undefined;
44
57
  /**
45
- * Top-level keys are checked against `data`; only path-shaped keys stay open.
46
- * A misspelled or wrongly typed top-level key is a compile error.
58
+ * Deep readonly view of page `data`. Static only — the runtime object is not
59
+ * frozen.
60
+ */
61
+ export type DeepReadonly<T, D extends number = 6> = [D] extends [never] ? T : T extends PagePrimitive ? T : T extends Function ? T : T extends readonly unknown[] ? {
62
+ readonly [K in keyof T]: DeepReadonly<T[K], PageReadonlyDepth[D]>;
63
+ } : T extends object ? {
64
+ readonly [K in keyof T]: DeepReadonly<T[K], PageReadonlyDepth[D]>;
65
+ } : T;
66
+ /**
67
+ * Tuple path into `data`, depth-capped so large page states stay completable.
68
+ */
69
+ export type DataPath<T, D extends number = 5> = [D] extends [never] ? never : T extends readonly (infer U)[] ? [number] | [number, ...DataPath<U, PageReadonlyDepth[D]>] : T extends object ? {
70
+ [K in keyof T & (string | number)]: [K] | (DataPath<T[K], PageReadonlyDepth[D]> extends infer Rest ? Rest extends readonly PropertyKey[] ? [K, ...Rest] : never : never);
71
+ }[keyof T & (string | number)] : never;
72
+ export type DataPathValue<T, P extends readonly PropertyKey[]> = T extends null | undefined ? never : T extends unknown ? P extends readonly [
73
+ infer K,
74
+ ...infer Rest
75
+ ] ? Rest extends readonly PropertyKey[] ? Rest['length'] extends 0 ? K extends keyof T ? T[K] : K extends number ? T extends readonly (infer U)[] ? U : never : never : K extends keyof T ? DataPathValue<T[K], Rest> : K extends number ? T extends readonly (infer U)[] ? DataPathValue<U, Rest> : never : never : never : never : never;
76
+ /**
77
+ * Top-level keys are checked against `data`. Nested writes use `setPath` or
78
+ * the unchecked `setDataPath`.
47
79
  */
48
80
  export type SetDataPatch<TData> = {
49
- [K in keyof TData]?: LazyInitField<TData[K]>;
50
- } & Partial<Record<PageDataPath, unknown>>;
51
- export interface PageInstance<TData extends Record<string, unknown> = Record<string, unknown>> {
52
- data: TData;
81
+ [K in keyof TData]?: TData[K];
82
+ };
83
+ type SetDataValue<TData, K> = K extends PageDataPath ? {
84
+ "LingXia type error": "use setPath or setDataPath for nested writes";
85
+ } : K extends keyof TData ? TData[K] | DeepReadonly<TData[K]> : {
86
+ "LingXia type error": "unknown data key";
87
+ };
88
+ export interface PageInstance<TData extends object = Record<string, unknown>> {
89
+ readonly data: {
90
+ readonly [K in keyof TData]: DeepReadonly<TData[K]>;
91
+ };
53
92
  route: string;
54
93
  /**
55
94
  * Available when this page was opened as a surface via
@@ -60,7 +99,14 @@ export interface PageInstance<TData extends Record<string, unknown> = Record<str
60
99
  * Available when this page was opened by `lx.navigateTo(...)`.
61
100
  */
62
101
  opener?: PageMessagePort;
63
- setData(data: SetDataPatch<TData>, callback?: () => void): void;
102
+ setData<TPatch extends Record<string, unknown>>(data: TPatch & {
103
+ [K in keyof TPatch]: SetDataValue<TData, K>;
104
+ }): void;
105
+ setPath<const P extends DataPath<TData>>(path: P, value: DataPathValue<TData, P>): void;
106
+ /** Nested write by a runtime-resolved string path. JSON shape is checked; the path/value relationship is not. */
107
+ setDataPath(path: string, value: JsonValue | undefined): void;
108
+ /** Drain pending state writes; an attached View acknowledges application, not paint. Rejects on unload. */
109
+ flush(): Promise<void>;
64
110
  }
65
111
  /**
66
112
  * Injected by the runtime into methods listed in `stream_handlers` page metadata.
@@ -76,6 +122,12 @@ export interface StreamHandle<T = unknown> {
76
122
  end(result?: unknown): void;
77
123
  /** End the stream with an error. */
78
124
  error(code: string, message?: string): void;
125
+ /**
126
+ * Called once if View cancels before `end`/`error`. Returns unsubscribe.
127
+ * The generator form still observes cancel in `finally`; use this for the
128
+ * callback-based handle.
129
+ */
130
+ onCancel(handler: () => void): () => void;
79
131
  }
80
132
  /**
81
133
  * Injected by the runtime as the second parameter when View opens a channel.
@@ -88,12 +140,12 @@ export interface ChannelHandle<TSend = unknown, TReceive = unknown> {
88
140
  send(payload: TSend): void;
89
141
  /** Close the channel from Logic side. */
90
142
  close(code?: string, reason?: string): void;
91
- /** Register a listener for incoming events. */
92
- on(event: 'data', handler: (payload: TReceive) => void): void;
143
+ /** Register a listener for incoming events. Returns unsubscribe. */
144
+ on(event: 'data', handler: (payload: TReceive) => void): () => void;
93
145
  on(event: 'close', handler: (info: {
94
146
  code: string;
95
147
  reason: string;
96
- }) => void): void;
148
+ }) => void): () => void;
97
149
  }
98
150
  /**
99
151
  * Download options.
@@ -107,30 +159,47 @@ export interface ChannelHandle<TSend = unknown, TReceive = unknown> {
107
159
  */
108
160
  export type DownloadOptions<TDestination extends DownloadDestination = DownloadDestination> = TDestination extends 'downloads' ? DownloadsDownloadOptions : AppDownloadOptions;
109
161
  export type DownloadResultForDestination<TDestination extends DownloadDestination> = TDestination extends 'downloads' ? DownloadsDownloadResult : AppDownloadResult;
110
- export interface DownloadProgressEvent<TResult extends DownloadResult = DownloadResult> {
111
- kind: 'progress' | 'paused' | 'resumed' | 'canceled' | 'completed';
162
+ export type DownloadProgressEvent<TResult extends DownloadResult = DownloadResult> = {
163
+ kind: 'progress' | 'paused' | 'resumed';
112
164
  downloadedBytes?: number;
113
165
  totalBytes?: number;
114
166
  /** Present only when the total size is known. */
115
167
  progress?: number;
116
- result?: TResult;
168
+ } | {
169
+ kind: 'canceled';
170
+ downloadedBytes?: number;
171
+ totalBytes?: number;
172
+ progress?: number;
173
+ } | {
174
+ kind: 'completed';
175
+ downloadedBytes?: number;
176
+ totalBytes?: number;
177
+ progress?: number;
178
+ result: TResult;
179
+ };
180
+ export type DownloadIteratorResult<TResult extends DownloadResult = DownloadResult> = IteratorResult<DownloadProgressEvent<TResult>, void>;
181
+ export type AppTempDownloadResult = Extract<AppDownloadResult, {
182
+ storage: 'temp';
183
+ }>;
184
+ export type AppPersistedDownloadResult = Extract<AppDownloadResult, {
185
+ storage: 'userdata';
186
+ }>;
187
+ export type ChooseFileSingleResult = {
188
+ status: 'ok';
189
+ paths: [string];
190
+ } | CanceledResult;
191
+ /** A running operation. Progress has one consumer; stopping observation does not cancel it. */
192
+ export interface Task<TResult, TProgress> {
193
+ readonly result: Promise<TResult>;
194
+ readonly progress: AsyncIterable<TProgress>;
117
195
  }
118
- export interface DownloadIteratorResult<TResult extends DownloadResult = DownloadResult> {
119
- done: boolean;
120
- value?: DownloadProgressEvent<TResult>;
196
+ export interface CancelableTask<TResult, TProgress> extends Task<TResult, TProgress> {
197
+ /** Requests cancellation. Await result to observe the terminal outcome. */
198
+ cancel(): Promise<void>;
121
199
  }
122
- export interface DownloadTask<TDownloadResult extends DownloadResult = DownloadResult> extends PromiseLike<TDownloadResult>, AsyncIterable<DownloadProgressEvent<TDownloadResult>> {
123
- next(): Promise<DownloadIteratorResult<TDownloadResult>>;
124
- /** Stops iteration only. Does not cancel the underlying download task. */
125
- return(): Promise<DownloadIteratorResult<TDownloadResult>>;
126
- catch<TRejected = never>(onrejected?: ((reason: unknown) => TRejected | PromiseLike<TRejected>) | null): Promise<TDownloadResult | TRejected>;
127
- finally(onfinally?: (() => void) | null): Promise<TDownloadResult>;
200
+ export interface DownloadTask<TResult extends DownloadResult = DownloadResult> extends CancelableTask<TResult, DownloadProgressEvent<TResult>> {
128
201
  pause(): Promise<void>;
129
202
  resume(): Promise<void>;
130
- cancel(): Promise<void>;
131
- /** Alias for cancel(), matching browser/mini-program abort naming. */
132
- abort(): Promise<void>;
133
- wait(): Promise<TDownloadResult>;
134
203
  }
135
204
  declare global {
136
205
  /**
@@ -151,28 +220,44 @@ declare global {
151
220
  readonly env: HostAppEnv;
152
221
  /**
153
222
  * Launch-at-startup control. Absent where the host cannot register a
154
- * startup item; its presence and `lx.supports({ capability: 'autostart' })` always
155
- * agree, so `lx.app.autostart?.…` and the query are interchangeable.
223
+ * startup item; its presence and `lx.supports('app.autostart')` always
224
+ * agree, so `lx.host.autostart?.…` and the query are interchangeable.
156
225
  */
157
226
  autostart?: AutostartApi;
227
+ /**
228
+ * Local notifications. Absent where the host cannot post them; its presence
229
+ * and `lx.supports('app.notification')` always agree.
230
+ */
231
+ notification?: NotificationApi;
232
+ /**
233
+ * Product-drawn desktop banner (top-right). Absent off desktop and in
234
+ * guest lxapps; its presence and `lx.supports('app.banner')`
235
+ * always agree.
236
+ */
237
+ banner?: BannerApi;
158
238
  /** The language this lxapp renders in. Every lxapp follows it. */
159
239
  readonly displayLanguage: DisplayLanguageApi;
160
240
  /** The light/dark scheme this lxapp renders in. */
161
241
  readonly appearance: AppearanceApi;
162
242
  /**
163
243
  * Product-wide settings, and their single writer. Present only in the
164
- * Control app the host sealed at build time; its presence and
165
- * `lx.supports({ capability: 'control' })` always agree, so
166
- * `lx.app.control?.…` and the query are interchangeable.
244
+ * Control app the host sealed at build time. Use
245
+ * `lx.host.control !== undefined` to inspect that identity.
167
246
  */
168
247
  readonly control?: ControlApi;
169
248
  /**
170
249
  * Product-wide cache reporting and clearing for a settings screen.
171
- * Present only in the Control app; its presence and
172
- * `lx.supports({ capability: 'control' })` always agree, so
173
- * `lx.app.cache?.…` and the query are interchangeable.
250
+ * Present only in the Control app; presence agrees with
251
+ * `lx.host.control !== undefined`.
174
252
  */
175
253
  cache?: AppCacheApi;
254
+ /**
255
+ * Take over host updates for the rest of this process. Irreversible: the
256
+ * built-in auto-flow will not prompt or download again, including after
257
+ * the calling page unloads. Does not cancel an already-started update task.
258
+ * `update.apply()` claims as well. `checkUpdate()` does not.
259
+ */
260
+ claimCustomUpdate(): void;
176
261
  }
177
262
  /** Runtime environment constants backed by abstract `lx://` paths. */
178
263
  interface LxEnv {
@@ -181,15 +266,37 @@ declare global {
181
266
  /**
182
267
  * Terminal product settings. Present only in the host-bundled Terminal
183
268
  * Settings lxapp when the host declares `capabilities.terminal`; its
184
- * presence and `lx.supports({ capability: 'terminal' })` always agree.
269
+ * presence and `lx.supports('terminal')` always agree.
185
270
  */
186
271
  readonly terminal?: TerminalApi;
187
272
  /** Download to the downloads directory. */
188
273
  downloadFile(options: DownloadsDownloadOptions): DownloadTask<DownloadsDownloadResult>;
274
+ /** Download to a durable app-owned path. */
275
+ downloadFile(options: AppDownloadOptions & {
276
+ filePath: string;
277
+ }): DownloadTask<AppPersistedDownloadResult>;
278
+ /** Download to a temporary app-owned path. */
279
+ downloadFile(options: AppDownloadOptions & {
280
+ filePath?: undefined;
281
+ }): DownloadTask<AppTempDownloadResult>;
189
282
  /** Download to the lxapp-managed app directory. */
190
283
  downloadFile(options: AppDownloadOptions): DownloadTask<AppDownloadResult>;
191
284
  /** Download with a destination-correlated result type. */
192
285
  downloadFile<TDestination extends DownloadDestination = "app">(options: DownloadOptions<TDestination>): DownloadTask<DownloadResultForDestination<TDestination>>;
286
+ /** Single-file picker: a completed selection is exactly one path. */
287
+ chooseFile(options: ChooseFileOptions & {
288
+ multiple: false;
289
+ }): Promise<ChooseFileSingleResult>;
290
+ chooseFile(options: ChooseFileOptions & {
291
+ multiple: true;
292
+ }): Promise<ChooseFileResult>;
293
+ /**
294
+ * Opens a file picker.
295
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the picker. A
296
+ * completed selection resolves `{ status: 'ok', paths }` with at least one
297
+ * path. Rejects when the picker fails or returns an invalid payload.
298
+ */
299
+ chooseFile(options?: ChooseFileOptions): Promise<ChooseFileResult>;
193
300
  /**
194
301
  * Open this lxapp's store with every key's shape pinned on the handle.
195
302
  * `get` / `set` / `delete` then share that schema instead of
@@ -207,7 +314,7 @@ declare global {
207
314
  export type StorageSchema = object;
208
315
  type StorageKey<S extends object> = Extract<keyof S, string>;
209
316
  type StorageEntry<S extends object> = {
210
- [K in StorageKey<S>]: [key: K, value: S[K]];
317
+ [K in StorageKey<S>]: [key: K, value: S[K] | DeepReadonly<S[K]>];
211
318
  }[StorageKey<S>];
212
319
  /**
213
320
  * Schema-typed view of the same store `lx.getStorage()` returns.
@@ -220,7 +327,7 @@ type StorageEntry<S extends object> = {
220
327
  * unchecked assertion this type exists to remove from `get<T>()`.
221
328
  */
222
329
  export type TypedStorage<S extends object> = {
223
- get<K extends StorageKey<S>>(key: K): Promise<S[K] | undefined>;
330
+ get<K extends StorageKey<S>>(key: K, decode?: (value: unknown) => S[K]): Promise<S[K] | undefined>;
224
331
  set(...entry: StorageEntry<S>): Promise<void>;
225
332
  has(key: StorageKey<S>): Promise<boolean>;
226
333
  delete(key: StorageKey<S>): Promise<void>;
@@ -229,21 +336,23 @@ export type TypedStorage<S extends object> = {
229
336
  info(): Promise<StorageInfo>;
230
337
  };
231
338
  /**
232
- * Result of `lx.showActionSheet`. Branch on `canceled` before reading
233
- * the selected item index.
339
+ * Result of `lx.showActionSheet`. Branch on `status` before reading
340
+ * the selected item id.
234
341
  */
235
342
  export type ActionSheetResult = {
236
- canceled: false;
237
- /** Index of the tapped item in `itemList`. */
238
- index: number;
343
+ status: 'ok';
344
+ /** Stable id of the selected action. */
345
+ id: string;
239
346
  } | CanceledResult;
347
+ /** An acknowledgement dialog has no cancel button. */
348
+ export type AlertOptions = Omit<ShowModalOptions, 'showCancel' | 'cancelText' | 'cancelColor'>;
240
349
  /** Every surface handle, narrowable by `kind`. */
241
350
  export type AnySurface = PageSurface | DeclaredSurface | AppSurface | TabSurface | BuiltinSurface;
242
351
  /**
243
352
  * The product-wide cache a settings screen reports and clears.
244
353
  * App-scoped, not lxapp-scoped: the figure covers every lxapp the host
245
354
  * has run. Injected only into the Control app, same gate as
246
- * `lx.app.control` — guests do not have the member.
355
+ * `lx.host.control` — guests do not have the member.
247
356
  */
248
357
  export type AppCacheApi = {
249
358
  /** Estimated reclaimable managed bytes; excludes live session storage and WebView cache. */
@@ -278,7 +387,7 @@ export type AppDownloadOptions = DownloadOptionsBase & {
278
387
  /**
279
388
  * Optional app-owned durable output path.
280
389
  *
281
- * Omit `filePath` to receive a temporary result in `tempFilePath`. Relative
390
+ * Omit `filePath` to receive a temporary result in `uri`. Relative
282
391
  * paths resolve under user data. `lx://` paths must target `lx://userdata`;
283
392
  * `lx://usercache` is not accepted here.
284
393
  */
@@ -289,24 +398,15 @@ export type AppDownloadOptions = DownloadOptionsBase & {
289
398
  destination?: 'app';
290
399
  };
291
400
  export type AppDownloadResult = {
292
- /**
293
- * Temporary result.
294
- *
295
- * Not durable; move or copy it to `lx://userdata` if you need to keep it.
296
- *
297
- * When `filePath` is omitted, the runtime must be able to infer a file
298
- * type from the URL or the server's `Content-Type` header.
299
- */
300
- tempFilePath: string;
301
- filePath?: never;
401
+ uri: AppDownloadFilePath;
402
+ storage: 'temp';
302
403
  mimeType?: string;
303
- size: number;
404
+ sizeBytes: number;
304
405
  } | {
305
- /** Durable destination under `lx://userdata`. */
306
- filePath: AppDownloadFilePath;
307
- tempFilePath?: never;
406
+ uri: AppDownloadFilePath;
407
+ storage: 'userdata';
308
408
  mimeType?: string;
309
- size: number;
409
+ sizeBytes: number;
310
410
  };
311
411
  export type AppInstance = AppConfig & {
312
412
  globalData: Record<string, unknown>;
@@ -350,7 +450,7 @@ export type AppScreenshotOptions = {
350
450
  };
351
451
  export type AppScreenshotResult = {
352
452
  /** `lx://` URI of the captured PNG in the lxapp temp directory. */
353
- tempFilePath: string;
453
+ uri: string;
354
454
  /** Image width in pixels, when the runtime could read it from the PNG. */
355
455
  width?: number;
356
456
  /** Image height in pixels, when the runtime could read it from the PNG. */
@@ -361,7 +461,7 @@ export type AppSurface = SurfaceBase & SurfaceShowable & {
361
461
  readonly kind: 'app';
362
462
  readonly realized: 'main' | 'aside';
363
463
  };
364
- /** `lx.app.appearance` — the scheme this lxapp renders in. */
464
+ /** `lx.host.appearance` — the scheme this lxapp renders in. */
365
465
  export type AppearanceApi = {
366
466
  /**
367
467
  * The scheme this lxapp is rendering in. An lxapp that pinned one in its
@@ -381,25 +481,6 @@ export type AppearanceApi = {
381
481
  * the system.
382
482
  */
383
483
  export type AppearancePreference = 'auto' | 'light' | 'dark';
384
- /**
385
- * Launch-at-startup control for the host app.
386
- * Absent (`undefined`) wherever the host cannot register a startup item.
387
- * `lx.supports({ capability: 'autostart' })` and the member's presence always
388
- * agree, so either gate works:
389
- * ```ts
390
- * if (lx.supports({ capability: 'autostart' })) {
391
- * // render the "Launch at startup" toggle
392
- * }
393
- * ```
394
- * Requires `capabilities.autostart: true` in `lingxia.yaml`; without it the
395
- * member is absent on all platforms. Declaring the capability never enables
396
- * autostart by itself — the SDK registers the app only when `setEnabled(true)`
397
- * is called, so the decision stays with the user (typically a settings-page
398
- * toggle, default off).
399
- * Host-app-level capability: like `checkUpdate` and `screenshot`, the methods
400
- * are available only to the native-assigned Control app; other lxapps receive
401
- * a permission error.
402
- */
403
484
  export type AutostartApi = {
404
485
  /**
405
486
  * Whether the app is currently registered to launch at startup, read from
@@ -416,6 +497,40 @@ export type AutostartApi = {
416
497
  */
417
498
  setEnabled(on: boolean): Promise<void>;
418
499
  };
500
+ /**
501
+ * Product-drawn desktop banner, top-right. Not an OS notification and
502
+ * not bound to App Link. Present only in the desktop Control app;
503
+ * presence and `lx.supports('app.banner')` always agree.
504
+ * No buttons: an informational card that auto-dismisses (5s unless
505
+ * `timeoutMs` is set). With buttons: a gate that waits for a choice,
506
+ * dismiss, timeout, or replace. User outcomes resolve; presentation
507
+ * failures reject.
508
+ */
509
+ export type BannerApi = {
510
+ show(options: {
511
+ id?: string;
512
+ title: string;
513
+ body?: string;
514
+ actions?: Array<{
515
+ id: string;
516
+ label: string;
517
+ style?: 'default' | 'primary' | 'destructive';
518
+ }>;
519
+ timeoutMs?: number;
520
+ /** Omit/`system` follows the OS. `light`/`dark` force chrome. `#RGB`/`#RRGGBB`/`#RRGGBBAA` is a solid fill. */
521
+ background?: 'system' | 'light' | 'dark' | string;
522
+ }): Promise<{
523
+ status: 'ok';
524
+ id: string;
525
+ action: string;
526
+ } | {
527
+ status: 'canceled';
528
+ id: string;
529
+ reason: 'dismissed' | 'timeout' | 'replaced';
530
+ }>;
531
+ /** Unknown ids are fine. */
532
+ dismiss(id: string): Promise<void>;
533
+ };
419
534
  export type BinaryFileData = ArrayBuffer | ArrayBufferView;
420
535
  /**
421
536
  * Built-in browser product page. Opening one requires
@@ -425,26 +540,25 @@ export type BuiltinShellPage = 'downloads';
425
540
  /**
426
541
  * A host builtin page such as downloads. The shell owns
427
542
  * its lifetime and its visibility, so this handle reports identity:
428
- * there is no `show` / `hide`, and the inherited `close()` rejects
429
- * with `unsupported_placement`.
543
+ * there is no `show`, `hide`, `close`, or `onClose`.
430
544
  */
431
- export type BuiltinSurface = SurfaceBase & {
545
+ export type BuiltinSurface = Omit<SurfaceBase, 'close' | 'onClose'> & {
432
546
  readonly kind: 'builtin';
433
547
  };
434
548
  /** The user dismissed the operation. Never an error. */
435
549
  export type CanceledResult = {
436
- canceled: true;
550
+ status: 'canceled';
437
551
  };
438
552
  export type ChooseDirectoryOptions = {
439
553
  /** Initial directory the dialog opens in. Platform default if omitted. */
440
554
  defaultPath?: string;
441
555
  };
442
556
  /**
443
- * Result of `lx.chooseDirectory`. Branch on `canceled` before reading
557
+ * Result of `lx.chooseDirectory`. Branch on `status` before reading
444
558
  * the selected directory.
445
559
  */
446
560
  export type ChooseDirectoryResult = {
447
- canceled: false;
561
+ status: 'ok';
448
562
  /** Native-consumable directory reference (path or URI). */
449
563
  path: string;
450
564
  } | CanceledResult;
@@ -462,11 +576,11 @@ export type ChooseFileOptions = {
462
576
  defaultPath?: string;
463
577
  };
464
578
  /**
465
- * Result of `lx.chooseFile`. Branch on `canceled` before reading the
579
+ * Result of `lx.chooseFile`. Branch on `status` before reading the
466
580
  * selected paths.
467
581
  */
468
582
  export type ChooseFileResult = {
469
- canceled: false;
583
+ status: 'ok';
470
584
  /**
471
585
  * File paths returned by LingXia; always at least one. Values may be
472
586
  * app-local paths, `lx://...` paths, or platform system-picker references.
@@ -480,22 +594,79 @@ export type ChooseMediaOptions = {
480
594
  mediaType?: ('image' | 'video')[];
481
595
  sourceType?: ('album' | 'camera')[];
482
596
  camera?: 'back' | 'front';
483
- maxDuration?: number;
597
+ maxDurationSeconds?: number;
484
598
  };
485
599
  /**
486
- * Result of `lx.chooseMedia`. Branch on `canceled` before reading the
600
+ * Result of `lx.chooseMedia`. Branch on `status` before reading the
487
601
  * selected entries.
488
602
  */
489
603
  export type ChooseMediaResult = {
490
- canceled: false;
604
+ status: 'ok';
491
605
  /** Picked media; always at least one entry. */
492
606
  entries: [ChosenMediaEntry, ...ChosenMediaEntry[]];
493
607
  } | CanceledResult;
494
608
  export type ChosenMediaEntry = {
495
- tempFilePath: string;
609
+ uri: string;
496
610
  fileType: 'image' | 'video';
497
611
  isOriginal: boolean;
498
612
  };
613
+ export type ClipboardApi = globalThis.ClipboardApi;
614
+ export type ClipboardItem = {
615
+ type: 'text';
616
+ text: string;
617
+ } | {
618
+ type: 'image';
619
+ /**
620
+ * Temporary `lx://temp` PNG, session-scoped and auto-cleaned. Move or
621
+ * copy it with `lx.fs` if you need to keep it.
622
+ */
623
+ filePath: string;
624
+ };
625
+ export type ClipboardReadOptions = {
626
+ /** Omit to receive every representation the host can surface. */
627
+ type?: ClipboardType;
628
+ };
629
+ /**
630
+ * Result of `lx.clipboard.read`. Omit `type` to receive every
631
+ * representation this host can surface. A requested type that is
632
+ * absent is `{ status: 'empty' }`, not an error.
633
+ */
634
+ export type ClipboardReadResult = {
635
+ status: 'empty';
636
+ } | {
637
+ status: 'ok';
638
+ items: ClipboardItem[];
639
+ } | CanceledResult;
640
+ /**
641
+ * Result of `lx.clipboard.readText`. Branch on `status`.
642
+ * A copied empty string is `{ status: 'ok', text: '' }`;
643
+ * an image-only clipboard is `{ status: 'empty' }`.
644
+ */
645
+ export type ClipboardTextResult = {
646
+ status: 'empty';
647
+ } | {
648
+ status: 'ok';
649
+ text: string;
650
+ } | CanceledResult;
651
+ /** Representations the runtime can round-trip. Closed union. */
652
+ export type ClipboardType = 'text' | 'image';
653
+ /**
654
+ * One clipboard write. Every host accepts both: `image` takes a PNG or
655
+ * JPEG file and re-encodes it as the platform's native image format.
656
+ */
657
+ export type ClipboardWriteItem = {
658
+ type: 'text';
659
+ /** Unicode text. Rejects `E_INVALID_ARG` when larger than 1 MiB. */
660
+ text: string;
661
+ } | {
662
+ type: 'image';
663
+ /**
664
+ * Managed `lx://` path, or a picker result from `lx.chooseFile` /
665
+ * `lx.chooseMedia` — the same file rules as `lx.share`. Rejects
666
+ * `E_INVALID_ARG` when the file is not a decodable image.
667
+ */
668
+ filePath: string;
669
+ };
499
670
  export type CompressImageOptions = {
500
671
  path: string;
501
672
  quality?: number;
@@ -503,25 +674,30 @@ export type CompressImageOptions = {
503
674
  compressedHeight?: number;
504
675
  };
505
676
  export type CompressImageResult = {
506
- tempFilePath: string;
507
- };
508
- export type CompressVideoIteratorResult = {
509
- done: boolean;
510
- value?: CompressVideoProgressEvent;
677
+ uri: string;
511
678
  };
679
+ export type CompressVideoIteratorResult = IteratorResult<CompressVideoProgressEvent, void>;
512
680
  export type CompressVideoOptions = {
681
+ signal?: AbortSignal;
513
682
  /**
514
683
  * Source video path or `lx://` URI.
515
684
  */
516
685
  path: string;
517
686
  /**
518
- * Cross-platform note: video compression parameters are best-effort and may map to
519
- * native presets instead of exact encoder settings.
520
- *
687
+ * Optional output path for compressed file.
688
+ */
689
+ outputPath?: string;
690
+ } & ({
691
+ /**
521
692
  * Compression quality preset.
522
- * When provided, `bitrate`, `fps`, and `resolution` are ignored.
693
+ * Mutually exclusive with `bitrate`, `fps`, and `resolution`.
523
694
  */
524
- quality?: VideoCompressQuality;
695
+ quality: VideoCompressQuality;
696
+ bitrate?: never;
697
+ fps?: never;
698
+ resolution?: never;
699
+ } | {
700
+ quality?: never;
525
701
  /**
526
702
  * Preferred target video bitrate in kbps.
527
703
  * May be adjusted or ignored by platform codec/runtime limitations.
@@ -537,17 +713,13 @@ export type CompressVideoOptions = {
537
713
  * May be approximated or ignored by platform transcoder capabilities.
538
714
  */
539
715
  resolution?: number;
540
- /**
541
- * Optional output path for compressed file.
542
- */
543
- outputPath?: string;
544
- };
716
+ });
545
717
  export type CompressVideoProgressEvent = {
546
718
  /** Transcode progress in percent, `0`-`100`. */
547
719
  progress: number;
548
720
  };
549
721
  export type CompressVideoResult = {
550
- tempFilePath: string;
722
+ uri: string;
551
723
  width: number;
552
724
  height: number;
553
725
  durationMs: number;
@@ -560,23 +732,11 @@ export type CompressVideoResult = {
560
732
  };
561
733
  /**
562
734
  * Handle returned by `lx.compressVideo`.
563
- * Awaiting the task resolves with the final {@link CompressVideoResult}.
564
- * Iterating it with `for await` yields {@link CompressVideoProgressEvent}s
735
+ * Awaiting `task.result` resolves with the final {@link CompressVideoResult}.
736
+ * Iterating `task.progress` with `for await` yields {@link CompressVideoProgressEvent}s
565
737
  * while the transcode runs.
566
738
  */
567
- export type CompressVideoTask = PromiseLike<CompressVideoResult> & AsyncIterable<CompressVideoProgressEvent> & {
568
- next(): Promise<CompressVideoIteratorResult>;
569
- /** Stops iteration only. Does not cancel the compression. */
570
- return(): Promise<CompressVideoIteratorResult>;
571
- catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<CompressVideoResult | TResult>;
572
- finally(onfinally?: (() => void) | null): Promise<CompressVideoResult>;
573
- /**
574
- * Cancels the transcode and deletes any partial output.
575
- * The task promise rejects with an `AbortError` (`code: 'E_ABORT'`).
576
- */
577
- cancel(): void;
578
- wait(): Promise<CompressVideoResult>;
579
- };
739
+ export type CompressVideoTask = CancelableTask<CompressVideoResult, CompressVideoProgressEvent>;
580
740
  /**
581
741
  * Configured page name from `lxapp.json` / `lingxia.yaml`. JavaScript
582
742
  * navigation accepts only this name; full routes such as
@@ -587,16 +747,18 @@ export type CompressVideoTask = PromiseLike<CompressVideoResult> & AsyncIterable
587
747
  * a project that never ran a build still compiles.
588
748
  */
589
749
  export type ConfiguredPageName = keyof LxAppPages extends never ? string : keyof LxAppPages;
750
+ /** A confirmation dialog always permits declining. */
751
+ export type ConfirmOptions = Omit<ShowModalOptions, 'showCancel'>;
590
752
  export type ConnectWifiOptions = {
591
- SSID: string;
753
+ ssid: string;
592
754
  password?: string;
593
755
  };
594
756
  /**
595
- * `lx.app.control` — product-wide settings, and their single writer.
757
+ * `lx.host.control` — product-wide settings, and their single writer.
596
758
  * Present only in the Control app. Bind it once rather than repeating
597
- * `lx.app.control!`:
759
+ * `lx.host.control!`:
598
760
  * ```js
599
- * const control = lx.app.control;
761
+ * const control = lx.host.control;
600
762
  * if (!control) return; // not the Control app
601
763
  * await control.appearance.setPreference('dark');
602
764
  * ```
@@ -605,7 +767,7 @@ export type ControlApi = {
605
767
  readonly displayLanguage: ControlDisplayLanguageApi;
606
768
  readonly appearance: ControlAppearanceApi;
607
769
  };
608
- /** `lx.app.control.appearance` — the product's own light/dark setting. */
770
+ /** `lx.host.control.appearance` — the product's own light/dark setting. */
609
771
  export type ControlAppearanceApi = {
610
772
  /** What the user chose for the whole product. */
611
773
  getPreference(): AppearancePreference;
@@ -616,14 +778,14 @@ export type ControlAppearanceApi = {
616
778
  setPreference(preference: AppearancePreference): Promise<void>;
617
779
  /**
618
780
  * Follow the choice, not what it resolves to: a system flip under `'auto'`
619
- * moves `lx.app.appearance.watch` and leaves this quiet. Starts with the
781
+ * moves `lx.host.appearance.watch` and leaves this quiet. Starts with the
620
782
  * current value; that first callback runs synchronously, before
621
783
  * `watchPreference` returns.
622
784
  */
623
785
  watchPreference(callback: (preference: AppearancePreference) => void): () => void;
624
786
  };
625
787
  /**
626
- * `lx.app.control.displayLanguage` — the preference behind that
788
+ * `lx.host.control.displayLanguage` — the preference behind that
627
789
  * language, for the one surface that edits it.
628
790
  */
629
791
  export type ControlDisplayLanguageApi = {
@@ -648,7 +810,7 @@ export type DeviceOrientation = "portrait" | "landscape";
648
810
  export type DeviceOrientationChangeEvent = {
649
811
  value: DeviceOrientation;
650
812
  };
651
- /** `lx.app.displayLanguage` — the language this lxapp renders in. */
813
+ /** `lx.host.displayLanguage` — the language this lxapp renders in. */
652
814
  export type DisplayLanguageApi = {
653
815
  /**
654
816
  * The language in effect right now, as a canonical BCP-47 tag. Map it to
@@ -685,7 +847,7 @@ export type DownloadOptionsBase = {
685
847
  */
686
848
  headers?: Record<string, string>;
687
849
  /** Request timeout in milliseconds. */
688
- timeout?: number;
850
+ timeoutMs?: number;
689
851
  /** Optional abort signal. */
690
852
  signal?: AbortSignal;
691
853
  };
@@ -695,16 +857,16 @@ export type DownloadsDownloadOptions = DownloadOptionsBase & {
695
857
  * Optional filename hint for the system Downloads destination.
696
858
  * This is not an app-owned `lx.fs` path.
697
859
  */
698
- filePath?: string;
860
+ suggestedName?: string;
861
+ filePath?: never;
699
862
  /** Save into the user's system Downloads directory. */
700
863
  destination: 'downloads';
701
864
  };
702
865
  export type DownloadsDownloadResult = {
703
- /** Native system Downloads path. Do not pass this to `lx.fs`. */
704
- filePath: SystemDownloadsPath;
705
- tempFilePath?: never;
866
+ uri: SystemDownloadsPath;
867
+ storage: 'downloads';
706
868
  mimeType?: string;
707
- size: number;
869
+ sizeBytes: number;
708
870
  };
709
871
  /**
710
872
  * Configured page name belonging to *another* lxapp. This app's own
@@ -745,7 +907,7 @@ export type ExtractVideoThumbnailResult = {
745
907
  /**
746
908
  * Generated thumbnail file path.
747
909
  */
748
- tempFilePath: string;
910
+ uri: string;
749
911
  /**
750
912
  * Output image width in pixels.
751
913
  */
@@ -795,10 +957,10 @@ export type GetImageInfoOptions = {
795
957
  };
796
958
  /** Location APIs. */
797
959
  export type GetLocationOptions = {
798
- type?: 'wgs84' | 'gcj02';
960
+ coordinateSystem?: 'wgs84' | 'gcj02';
799
961
  altitude?: boolean;
800
962
  isHighAccuracy?: boolean;
801
- highAccuracyExpireTime?: number;
963
+ timeoutMs?: number;
802
964
  };
803
965
  export type GetVideoInfoOptions = {
804
966
  /**
@@ -832,7 +994,7 @@ export type HostAppUpdateEvent = {
832
994
  downloadedBytes?: number;
833
995
  progress?: number;
834
996
  } | {
835
- state: 'downloaded' | 'installRequested';
997
+ state: 'downloaded' | 'installRequested' | 'storeOpened';
836
998
  } | {
837
999
  state: 'failed';
838
1000
  stage: HostAppUpdateApplyStage;
@@ -842,38 +1004,36 @@ export type HostAppUpdateInfo = {
842
1004
  version: string;
843
1005
  size?: number;
844
1006
  releaseNotes?: string[];
845
- isForceUpdate: boolean;
846
1007
  /**
847
- * Download and apply this checked update.
1008
+ * How this update is applied. `store` opens the platform marketplace;
1009
+ * `direct` downloads and self-installs. `lx.supports('app.selfUpdate')`
1010
+ * is true only for `direct`.
1011
+ */
1012
+ channel: 'direct' | 'store';
1013
+ /**
1014
+ * Apply this checked update.
848
1015
  *
849
- * `apply()` is single-use for this update object.
1016
+ * `apply()` is single-use for this update object. It also claims custom
1017
+ * host updates for the rest of this process, same as
1018
+ * {@link HostAppApi.claimCustomUpdate}.
850
1019
  *
851
- * The returned task can be awaited directly when progress is not needed, or
852
- * consumed with `for await...of` to render progress.
1020
+ * Await `task.result` when progress is not needed, or iterate
1021
+ * `task.progress` to render it.
853
1022
  *
854
- * Requires `lx.supports({ capability: 'selfUpdate' })`. Where the host cannot
855
- * install its own update it rejects with an unsupported-operation error;
856
- * use `version` and `releaseNotes` to guide users to the app marketplace.
1023
+ * On `direct`, downloads and hands off install. On `store`, opens the
1024
+ * platform store listing (no package is downloaded) and resolves
1025
+ * `storeOpened`; whether the user then updates is not reported.
857
1026
  */
858
1027
  apply(): HostAppUpdateTask;
859
1028
  };
860
- export type HostAppUpdateIteratorResult = {
861
- done: boolean;
862
- value?: HostAppUpdateEvent;
863
- };
1029
+ export type HostAppUpdateIteratorResult = IteratorResult<HostAppUpdateEvent, void>;
864
1030
  export type HostAppUpdateResult = {
865
- state: 'installRequested';
866
- };
867
- export type HostAppUpdateTask = PromiseLike<HostAppUpdateResult> & AsyncIterable<HostAppUpdateEvent> & {
868
- next(): Promise<HostAppUpdateIteratorResult>;
869
- /** Stops iteration only. It does not cancel an app update already handed to the platform. */
870
- return(): Promise<HostAppUpdateIteratorResult>;
871
- catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<HostAppUpdateResult | TResult>;
872
- finally(onfinally?: (() => void) | null): Promise<HostAppUpdateResult>;
873
- wait(): Promise<HostAppUpdateResult>;
1031
+ /** `storeOpened` on a `store` channel: the listing opened, nothing was installed. */
1032
+ state: 'installRequested' | 'storeOpened';
874
1033
  };
1034
+ export type HostAppUpdateTask = Task<HostAppUpdateResult, HostAppUpdateEvent>;
875
1035
  /**
876
- * Canonical platform-family label shared by `lx.app.getBaseInfo().os`
1036
+ * Canonical platform-family label shared by `lx.host.getBaseInfo().os`
877
1037
  * and `lx.getDeviceInfo().osName`. `"unknown"` is a non-product build.
878
1038
  */
879
1039
  export type HostOs = 'iOS' | 'macOS' | 'Android' | 'Windows' | 'Harmony' | 'unknown';
@@ -883,6 +1043,38 @@ export type InstalledTerminalFont = {
883
1043
  ligatures: boolean;
884
1044
  nerdIcons: boolean;
885
1045
  };
1046
+ /**
1047
+ * Launch-at-startup control for the host app.
1048
+ * Absent (`undefined`) wherever the host cannot register a startup item.
1049
+ * `lx.supports('app.autostart')` and the member's presence always
1050
+ * agree, so either gate works:
1051
+ * ```ts
1052
+ * if (lx.supports('app.autostart')) {
1053
+ * // render the "Launch at startup" toggle
1054
+ * }
1055
+ * ```
1056
+ * Requires `capabilities.autostart: true` in `lingxia.yaml`; without it the
1057
+ * member is absent on all platforms. Declaring the capability never enables
1058
+ * autostart by itself — the SDK registers the app only when `setEnabled(true)`
1059
+ * is called, so the decision stays with the user (typically a settings-page
1060
+ * toggle, default off).
1061
+ * Host-app-level capability: like `checkUpdate` and `screenshot`, the methods
1062
+ * are available only to the native-assigned Control app; other lxapps receive
1063
+ * a permission error.
1064
+ * Local notifications as a Control-app resume affordance.
1065
+ * Absent unless the host declared `capabilities.notifications` and the
1066
+ * platform implements the local API. Presence and
1067
+ * `lx.supports('app.notification')` always agree.
1068
+ * Declaring the capability never prompts; permission runs on
1069
+ * `requestPermission()` or the first `show()` that reaches the OS.
1070
+ * Control app only. Guest lxapps receive a permission error.
1071
+ * A JSON value: what a navigation route parameter may hold. Not a way
1072
+ * to smuggle a payload — every route declares its own parameter
1073
+ * schema, and the framework caps size, count, and nesting.
1074
+ */
1075
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
1076
+ [key: string]: JsonValue;
1077
+ };
886
1078
  /**
887
1079
  * Input event APIs.
888
1080
  * Platform support: Android only
@@ -902,34 +1094,7 @@ export type KeyEventCallback = (event: KeyEvent) => void;
902
1094
  export type LxAppEnvVersion = 'release' | 'draft';
903
1095
  /** LxApp metadata APIs. */
904
1096
  export type LxAppReleaseType = 'release' | 'draft';
905
- /** Boolean capability names accepted by `lx.supports`. */
906
- export type LxCapabilityFlag = 'control' | 'terminal' | 'autostart' | 'notifications' | 'browser' | 'proxy' | 'selfUpdate' | 'process' | 'appUse' | 'computerUse' | 'browserUse' | 'mediaCapture';
907
- /**
908
- * One capability question per call. The catalog is closed, so
909
- * completion enumerates it and a typo is a type error. `capability`
910
- * is the discriminant; only the `surface` branch accepts a `value`.
911
- * Two surface answers describe an *affordance*, not whether the call
912
- * succeeds: `tab` is "the host has an in-app browser" — without it a
913
- * url still opens, in the OS browser instead — and `aside` is "a
914
- * docked region exists right now", while a compact layout still opens
915
- * the url through the in-app browser's own chrome. Ask them to decide
916
- * what to render, not whether to call.
917
- * `chrome` qualifies a window and only a window: it asks whether this
918
- * host can produce that decoration, not merely a window.
919
- */
920
- export type LxCapabilityQuery = {
921
- capability: 'surface';
922
- value: 'window';
923
- chrome?: WindowChrome;
924
- } | {
925
- capability: 'surface';
926
- value: Exclude<LxSurfaceCapability, 'window'>;
927
- } | {
928
- capability: LxCapabilityFlag;
929
- };
930
1097
  export type LxEnv = globalThis.LxEnv;
931
- /** Surface placements accepted by `lx.supports`. */
932
- export type LxSurfaceCapability = 'main' | 'aside' | 'float' | 'window' | 'tab';
933
1098
  /** Device action APIs. */
934
1099
  export type MakePhoneCallOptions = {
935
1100
  phoneNumber: string;
@@ -937,11 +1102,11 @@ export type MakePhoneCallOptions = {
937
1102
  export type MediaObjectFit = 'cover' | 'contain' | 'fill' | 'fit';
938
1103
  export type MediaRotation = 0 | 90 | 180 | 270;
939
1104
  /**
940
- * Result of `lx.showModal`. `canceled: false` means the user confirmed;
1105
+ * Result of `lx.showModal`. `status: 'ok'` means the user confirmed;
941
1106
  * there is no third resolved outcome. Presentation failures reject.
942
1107
  */
943
1108
  export type ModalResult = {
944
- canceled: false;
1109
+ status: 'ok';
945
1110
  } | CanceledResult;
946
1111
  /**
947
1112
  * One app-declared action shown in the host-provided More affordance.
@@ -997,6 +1162,36 @@ export type NavigationBarStylePatch = {
997
1162
  foregroundColor?: string | null;
998
1163
  dividerColor?: string | null;
999
1164
  };
1165
+ /**
1166
+ * Where a notification tap, a menu item, or the tray goes.
1167
+ * `page` and `app` are the same contract as `lx.navigateTo` /
1168
+ * `lx.navigateToApp`: a configured page name and a query, ordinary
1169
+ * scene. `route` is a host-registered location that is not a page.
1170
+ * `appLink` is an `https://` product URL that is also a real inbound
1171
+ * App Link (`scene === 8003`). `activate` just brings the product
1172
+ * forward.
1173
+ * A branch carries its own fields and no others: a mixed target is a
1174
+ * parameter error, not a best guess.
1175
+ */
1176
+ export type NavigationTarget = {
1177
+ kind: 'activate';
1178
+ } | {
1179
+ kind: 'page';
1180
+ page: ConfiguredPageName;
1181
+ query?: PageQuery;
1182
+ } | {
1183
+ kind: 'app';
1184
+ appId: string;
1185
+ page?: ExternalPageName;
1186
+ query?: PageQuery;
1187
+ } | {
1188
+ kind: 'route';
1189
+ name: string;
1190
+ params?: Record<string, JsonValue>;
1191
+ } | {
1192
+ kind: 'appLink';
1193
+ url: string;
1194
+ };
1000
1195
  export type NetworkChangeCallback = (info: NetworkInfo) => void;
1001
1196
  export type NetworkInfo = {
1002
1197
  isConnected: boolean;
@@ -1006,6 +1201,72 @@ export type NetworkInfo = {
1006
1201
  };
1007
1202
  /** Network status APIs. */
1008
1203
  export type NetworkType = 'none' | 'unknown' | 'wifi' | '2g' | '3g' | '4g' | '5g' | 'ethernet';
1204
+ export type NotificationApi = {
1205
+ /**
1206
+ * Read the current permission without prompting. `'default'` means the
1207
+ * user has not been asked yet.
1208
+ */
1209
+ getPermission(): Promise<'granted' | 'denied' | 'default'>;
1210
+ /**
1211
+ * Ask for notification permission. Prompts where the OS has a prompt and
1212
+ * the user has not answered; otherwise reports the current setting.
1213
+ * Rejects when the prompt is left unanswered.
1214
+ */
1215
+ requestPermission(): Promise<'granted' | 'denied'>;
1216
+ /**
1217
+ * Post or replace a local notification. `id` is the replace key: anything
1218
+ * pending or delivered under it is replaced, whatever `status` comes back,
1219
+ * and its old tap target stops resolving. Omit `id` to get a generated
1220
+ * one. Omit `schedule`, or pass a time that is not in the future, to post
1221
+ * now.
1222
+ *
1223
+ * `target` is where the tap goes, and an omitted one means
1224
+ * `{ kind: 'activate' }` — bring the product forward, nothing else.
1225
+ * `{ kind: 'page' }` opens a page of this Control app the way
1226
+ * `lx.navigateTo` does. `{ kind: 'app' }` opens another lxapp the way
1227
+ * `lx.navigateToApp` does. `{ kind: 'route' }` names a location the host
1228
+ * registered at startup that is not a page. `{ kind: 'appLink' }` takes
1229
+ * an `https://` URL that is also a real inbound App Link
1230
+ * (`scene === 8003`). An unknown page or route, a parameter the route
1231
+ * did not declare, or a host that is not configured rejects here, before
1232
+ * anything is posted.
1233
+ *
1234
+ * `status` says what happened: `'posted'` — the OS accepted it for
1235
+ * display now, which is not a receipt that anyone saw or read it;
1236
+ * `'scheduled'` — queued with the OS for `schedule`; `'suppressed'` — an
1237
+ * immediate post while the product is already frontmost, where nothing is
1238
+ * posted and no permission is needed. A scheduled notification is
1239
+ * presented even if the product is frontmost when it fires.
1240
+ *
1241
+ * A tap resolves through the host, so a target that is gone by then — a
1242
+ * route the build no longer registers, a cancelled or replaced
1243
+ * notification, cleared app data — brings the product forward and says it
1244
+ * is unavailable rather than opening something else.
1245
+ */
1246
+ show(options: {
1247
+ id?: string;
1248
+ title: string;
1249
+ body?: string;
1250
+ target?: NavigationTarget;
1251
+ schedule?: {
1252
+ at: number;
1253
+ } | {
1254
+ delayMs: number;
1255
+ };
1256
+ /** No sound. The banner still appears. */
1257
+ silent?: boolean;
1258
+ }): Promise<{
1259
+ id: string;
1260
+ status: 'posted' | 'scheduled' | 'suppressed';
1261
+ }>;
1262
+ /**
1263
+ * Remove what is pending or delivered under `id`, and retire its tap
1264
+ * target. Unknown ids are fine.
1265
+ */
1266
+ cancel(id: string): Promise<void>;
1267
+ /** Remove every local notification this API posted or scheduled. */
1268
+ cancelAll(): Promise<void>;
1269
+ };
1009
1270
  /** File system APIs. */
1010
1271
  export type OpenFileOptions = {
1011
1272
  /** Local file path or runtime-managed temp path. */
@@ -1058,7 +1319,7 @@ export type OpenPageShared = {
1058
1319
  size?: OverlaySurfaceSize;
1059
1320
  interaction?: SurfaceInteraction;
1060
1321
  query?: PageQuery;
1061
- /** Caller-owned identity, for `lx.surface.get(key)` later. */
1322
+ /** Caller-owned identity, for `lx.surface.getByKey(key)` later. */
1062
1323
  key?: string;
1063
1324
  };
1064
1325
  export type OpenUrlOptions = {
@@ -1070,7 +1331,7 @@ export type OpenUrlOptions = {
1070
1331
  /** Preferred docking side when the realized placement is an aside. */
1071
1332
  edge?: SurfaceEdge;
1072
1333
  size?: OverlaySurfaceSize;
1073
- /** Stable identity for `lx.surface.get(key)`. */
1334
+ /** Stable identity for `lx.surface.getByKey(key)`. */
1074
1335
  key?: string;
1075
1336
  };
1076
1337
  export type OverlaySurfaceSize = {
@@ -1109,6 +1370,11 @@ export type PageTargetOptions = {
1109
1370
  page: ConfiguredPageName;
1110
1371
  query?: PageQuery;
1111
1372
  };
1373
+ export type PickFileOptions = Omit<ChooseFileOptions, 'multiple'>;
1374
+ export type PickFileResult = {
1375
+ status: 'ok';
1376
+ uri: string;
1377
+ } | CanceledResult;
1112
1378
  export type PreviewMediaAdvance = 'manual' | 'next' | 'loop';
1113
1379
  /** One change-stream event / the `current` snapshot. */
1114
1380
  export type PreviewMediaChange = {
@@ -1117,28 +1383,17 @@ export type PreviewMediaChange = {
1117
1383
  };
1118
1384
  export type PreviewMediaCloseReason = 'manual' | 'completed' | 'interrupted' | 'error';
1119
1385
  /**
1120
- * Handle returned synchronously from `lx.previewMedia(...)` — synchronous so
1121
- * listeners can be attached before the first event fires:
1122
- * - `presented` resolves once the first pixel of the underlying media has
1123
- * been composited to screen. Use this to time the hide of an overlay
1124
- * surface above the preview so the swap is seamless. Never rejects;
1125
- * resolves with no value when the first frame is up. Safe to ignore.
1126
- * - `current` is a live `{ index, source }` snapshot of the item on screen,
1127
- * updated as the user swipes and as the session auto-advances.
1128
- * - `onChange(listener)` fires for every item change. Returns an
1129
- * unsubscribe function.
1130
- * - `completed` resolves `{ reason, index, source }` when the preview
1131
- * session ends (manual / auto / interrupted / error), or rejects on abort.
1132
- * If the call was aborted before any frame was presented, `presented` still
1133
- * resolves (with no value) once the abort takes effect — it never rejects,
1134
- * to keep fire-and-forget usage safe.
1135
- * @example
1136
- * const preview = lx.previewMedia({ sources, startIndex: 2 });
1137
- * preview.onChange(({ source }) => markAsViewed(source.path));
1138
- * const { reason, source } = await preview.completed;
1386
+ * A media session. `presented` distinguishes a rendered first frame from
1387
+ * an early close, cancellation, or failure. `completed` reports closure;
1388
+ * native playback errors may report reason `error`, while request failures reject.
1139
1389
  */
1140
1390
  export type PreviewMediaHandle = {
1141
- readonly presented: Promise<void>;
1391
+ readonly presented: Promise<{
1392
+ status: 'presented';
1393
+ } | {
1394
+ status: 'notPresented';
1395
+ reason: 'canceled' | 'failed' | 'closed';
1396
+ }>;
1142
1397
  readonly current: PreviewMediaChange;
1143
1398
  onChange(listener: (change: PreviewMediaChange) => void): () => void;
1144
1399
  readonly completed: Promise<PreviewMediaResult>;
@@ -1270,14 +1525,40 @@ export type ScanCodeOptions = {
1270
1525
  scanType?: ('barCode' | 'qrCode' | 'datamatrix' | 'pdf417')[];
1271
1526
  };
1272
1527
  /**
1273
- * Result of `lx.scanCode`. Branch on `canceled` before reading the scan
1528
+ * Result of `lx.scanCode`. Branch on `status` before reading the scan
1274
1529
  * payload.
1275
1530
  */
1276
1531
  export type ScanCodeResult = {
1277
- canceled: false;
1532
+ status: 'ok';
1278
1533
  scanResult: string;
1279
1534
  scanType: string;
1280
1535
  } | CanceledResult;
1536
+ /**
1537
+ * Where `lx.host.setBadge` paints.
1538
+ * `auto` (the default) marks every product-owned surface this platform
1539
+ * has: the dock and the menu-bar item on macOS, the taskbar and the
1540
+ * notification-area item on Windows, the home-screen icon on iOS and
1541
+ * HarmonyOS. Name one only when that surface is the point.
1542
+ * Asynchronous because it reports what actually happened: a platform that
1543
+ * answers through its own callback has to be waited for to be believed.
1544
+ * A surface with nothing to paint on is reported, not raised: a macOS
1545
+ * status item exists from the moment a tray is declared but stays hidden
1546
+ * until `lx.tray.show()`, and a badge on a hidden item is not a badge
1547
+ * anyone can see. That resolves `false` whether you named the surface or
1548
+ * took `auto`; only a malfunction rejects.
1549
+ * Apple ties the badge to notification permission. On macOS the label
1550
+ * always reaches the system, but the Dock declines to draw it for an app
1551
+ * that is registered with Notification Center and not allowed — so a host
1552
+ * that declares `capabilities.notifications` and never got a yes resolves
1553
+ * `false` here. A host that never asks is unaffected.
1554
+ * On iOS the home-screen badge is drawn by the notification system, so
1555
+ * it needs notification permission and only accepts a number — that is
1556
+ * the OS's rule, not an API coupling. Android has no cross-vendor
1557
+ * launcher badge at all, so `setBadge` returns `false` there.
1558
+ */
1559
+ export type SetBadgeOptions = {
1560
+ surface?: 'auto' | 'appIcon' | 'tray';
1561
+ };
1281
1562
  /** Share images, PDFs, or other files. */
1282
1563
  export type ShareFilesOptions = ShareTitleOptions & {
1283
1564
  /**
@@ -1401,7 +1682,7 @@ export type ShellOpenAppOptions = {
1401
1682
  */
1402
1683
  channel?: LxAppEnvVersion;
1403
1684
  targetVersion?: string;
1404
- /** Stable identity for `lx.surface.get(key)`. */
1685
+ /** Stable identity for `lx.surface.getByKey(key)`. */
1405
1686
  key?: string;
1406
1687
  };
1407
1688
  /**
@@ -1413,7 +1694,7 @@ export type ShellOpenAppOptions = {
1413
1694
  */
1414
1695
  export type ShellOpenDeclaredOptions = {
1415
1696
  /**
1416
- * Caller-owned identity, for `lx.surface.get(key)` later — the same key
1697
+ * Caller-owned identity, for `lx.surface.getByKey(key)` later — the same key
1417
1698
  * every opener takes. It carries one extra power here: a declaration can
1418
1699
  * be opened more than once, and the key is which instance you mean, so a
1419
1700
  * new key creates one. 1 to 128 UTF-8 bytes. Declarations without
@@ -1508,7 +1789,10 @@ export type ShellSurfacePatch = {
1508
1789
  edge?: SurfaceEdge;
1509
1790
  };
1510
1791
  export type ShowActionSheetOptions = {
1511
- itemList: string[];
1792
+ items: readonly {
1793
+ id: string;
1794
+ label: string;
1795
+ }[];
1512
1796
  itemColor?: string;
1513
1797
  };
1514
1798
  export type ShowModalOptions = {
@@ -1525,7 +1809,7 @@ export type ShowToastOptions = {
1525
1809
  title: string;
1526
1810
  icon?: 'success' | 'error' | 'loading' | 'none';
1527
1811
  image?: string;
1528
- duration?: number;
1812
+ durationMs?: number;
1529
1813
  mask?: boolean;
1530
1814
  position?: 'top' | 'center' | 'bottom';
1531
1815
  };
@@ -1542,7 +1826,7 @@ export type Storage = {
1542
1826
  * shape, exactly like a `JSON.parse` boundary; a missing key resolves
1543
1827
  * `undefined`, which a stored `null` never does.
1544
1828
  */
1545
- get<T = unknown>(key: string): Promise<T | undefined>;
1829
+ get<T = unknown>(key: string, decode?: (value: unknown) => T): Promise<T | undefined>;
1546
1830
  set(key: string, value: unknown): Promise<void>;
1547
1831
  /**
1548
1832
  * Resolves whether an exact key exists, without reading its value. Prefer
@@ -1565,7 +1849,7 @@ export type StorageInfo = {
1565
1849
  export type StreamSourceOptions = {
1566
1850
  provider: string;
1567
1851
  isLive: boolean;
1568
- duration?: number;
1852
+ durationSeconds?: number;
1569
1853
  params?: Record<string, unknown>;
1570
1854
  };
1571
1855
  /**
@@ -1584,19 +1868,16 @@ export type SurfaceApi = {
1584
1868
  */
1585
1869
  openDeclared(id: string): Promise<DeclaredSurface>;
1586
1870
  /**
1587
- * The live handle for a surface this lxapp opened **with a `key`**, found
1588
- * by that key or by its `id`. Removes the need to cache handles in order
1589
- * to reuse or close them. A surface opened without a `key` is not
1590
- * addressable — nothing else refers to a runtime-assigned id, so nothing
1591
- * registers it. A key you chose wins over an id it happens to spell.
1871
+ * Find a live surface by the explicit key passed when opening it.
1872
+ * Runtime-assigned ids are not lookup keys.
1592
1873
  */
1593
- get(keyOrId: string): AnySurface | undefined;
1874
+ getByKey(key: string): AnySurface | undefined;
1594
1875
  /**
1595
1876
  * Observe this presentation's viewport. Invoked immediately with the
1596
1877
  * current context, then again whenever it changes. Returns an unsubscribe
1597
1878
  * function.
1598
1879
  */
1599
- onContext(handler: (context: SurfaceContext) => void): () => void;
1880
+ watchContext(handler: (context: SurfaceContext) => void): () => void;
1600
1881
  };
1601
1882
  /** What every surface handle carries, whatever opened it. */
1602
1883
  export type SurfaceBase = {
@@ -1641,12 +1922,15 @@ export type SurfaceClosedEvent = {
1641
1922
  reason: SurfaceCloseReason;
1642
1923
  };
1643
1924
  /**
1644
- * The current surface viewport context, delivered to `lx.surface.onContext()`
1645
- * so an lxapp can self-adapt (e.g. switch column count by `sizeClass`).
1925
+ * The current surface viewport context, delivered to `lx.surface.watchContext()`
1926
+ * so an lxapp can choose a compact or workspace View. Column count and
1927
+ * spacing inside `regular` use CSS or the raw `width` / `height`.
1646
1928
  */
1647
1929
  export type SurfaceContext = {
1648
- /** compact (<600) / medium (600–840) / expanded (>840), with hysteresis. */
1649
- sizeClass: 'compact' | 'medium' | 'expanded';
1930
+ /** Whether the host layout currently offers a docked aside. */
1931
+ aside: boolean;
1932
+ /** compact (<600) / regular (≥600). Shell medium/expanded are not distinct here. */
1933
+ sizeClass: 'compact' | 'regular';
1650
1934
  /** Actual surface viewport width in logical pixels. */
1651
1935
  width: number;
1652
1936
  /** Actual surface viewport height in logical pixels. */
@@ -1777,31 +2061,29 @@ export type TabBarItemPatch = {
1777
2061
  badge?: string | null;
1778
2062
  redDot?: boolean;
1779
2063
  };
2064
+ /**
2065
+ * Patch for `lx.tabBar.update()`. Items, badges, red dots, and
2066
+ * visibility only — a `style` field is rejected. Colors stay in
2067
+ * static `lxapp.json` `tabBar.style`. `backgroundColor` is
2068
+ * mobile-only; the desktop sidebar follows the host
2069
+ * `lingxia.yaml` theme.
2070
+ */
1780
2071
  export type TabBarPatch = {
1781
2072
  visibility?: TabBarVisibilityPreference;
1782
- style?: TabBarStylePatch | null;
1783
2073
  items?: readonly TabBarItemPatch[];
1784
2074
  };
1785
- export type TabBarStylePatch = {
1786
- foregroundColor?: string | null;
1787
- selectedForegroundColor?: string | null;
1788
- };
1789
2075
  export type TabBarVisibilityPreference = 'auto' | 'visible' | 'hidden';
1790
2076
  /** External content in the in-app browser. */
1791
- export type TabSurface = SurfaceBase & {
2077
+ export type TabSurface = (SurfaceBase & {
1792
2078
  readonly kind: 'tab';
1793
2079
  readonly realized: 'tab' | 'aside';
1794
- /**
1795
- * `tab` when this handle owns exactly the tab it opened, and `close()` /
1796
- * `activate()` act on it. `group` when the browser chrome owns the tab
1797
- * strip: the content is open, but control belongs to that chrome, so both
1798
- * methods reject with `unsupported_placement`. Branch on this rather than
1799
- * on the old platform-dependent `null`.
1800
- */
1801
- readonly scope: 'tab' | 'group';
1802
- /** Bring this tab to the front of its browser. `scope: 'group'` rejects. */
2080
+ readonly scope: 'tab';
1803
2081
  activate(): Promise<void>;
1804
- };
2082
+ }) | (Omit<SurfaceBase, 'close' | 'onClose'> & {
2083
+ readonly kind: 'tab';
2084
+ readonly realized: 'tab' | 'aside';
2085
+ readonly scope: 'group';
2086
+ });
1805
2087
  export type TerminalApi = {
1806
2088
  /** Saved terminal settings, revision-checked on write. */
1807
2089
  readonly settings: TerminalSettingsApi;
@@ -1922,15 +2204,20 @@ export type TerminalThemeSettings = {
1922
2204
  light: string;
1923
2205
  dark: string;
1924
2206
  };
2207
+ export type ToastHandle = {
2208
+ /** Dismiss this toast only; harmless after a newer toast replaces it. */
2209
+ dismiss(): Promise<void>;
2210
+ };
1925
2211
  export type TrayApi = globalThis.TrayApi;
1926
2212
  /**
1927
2213
  * Runtime control of the menu-bar (macOS) / system-tray (Windows) status item.
1928
2214
  * The tray is declared in `lingxia.yaml` (`tray:`); these update its dynamic
1929
2215
  * content at runtime.
1930
2216
  * **Desktop only.** Mobile platforms have no tray, so every method here is a
1931
- * no-op there (it never throws) — safe to call from portable code. For an
1932
- * app-icon badge that *is* cross-platform (including mobile), use
1933
- * `lx.app.setBadge`.
2217
+ * no-op there (it never throws) — safe to call from portable code.
2218
+ * The tray belongs to the product, not to the lxapp that happens to be
2219
+ * running, so these are Control-app only: a guest lxapp calling one receives
2220
+ * a permission error.
1934
2221
  */
1935
2222
  export type TrayMenuItem = {
1936
2223
  label: string;
@@ -1948,7 +2235,10 @@ export type UpdateFailedInfo = UpdateReadyInfo & {
1948
2235
  /**
1949
2236
  * Callback-based updates for this lxapp's bundle. Available to every
1950
2237
  * lxapp. To update the native host app, the Control app uses the
1951
- * task-based `lx.app.checkUpdate()` API instead.
2238
+ * task-based `lx.host.checkUpdate()` API instead.
2239
+ * Listeners are a set: later subscriptions do not replace earlier ones.
2240
+ * The last pending ready/failed event is replayed to each new
2241
+ * subscriber until a newer event replaces it.
1952
2242
  */
1953
2243
  export type UpdateManager = {
1954
2244
  applyUpdate(): void;
@@ -1959,13 +2249,9 @@ export type UpdateManager = {
1959
2249
  };
1960
2250
  export type UpdateReadyInfo = {
1961
2251
  version?: string;
1962
- isForceUpdate?: boolean;
1963
2252
  channel?: "release" | "draft" | string;
1964
2253
  };
1965
- export type UploadIteratorResult = {
1966
- done: boolean;
1967
- value?: UploadProgressEvent;
1968
- };
2254
+ export type UploadIteratorResult = IteratorResult<UploadProgressEvent, void>;
1969
2255
  /**
1970
2256
  * Upload options. The file streams from disk, so the size ceiling is
1971
2257
  * the remote's, not memory.
@@ -1974,12 +2260,12 @@ export type UploadIteratorResult = {
1974
2260
  * - `multipart` (default) wraps the file in a `multipart/form-data`
1975
2261
  * envelope beside the `formData` text fields — what an ordinary form
1976
2262
  * endpoint parses. `name`, `fileName`, and `formData` describe that
1977
- * envelope.
2263
+ * envelope. `formData`, when present, must contain at least one field.
1978
2264
  * - `raw` sends the file bytes as the entire body. Presigned
1979
2265
  * object-storage URLs (S3, OSS, Azure Blob) need this: a multipart
1980
2266
  * envelope would be stored verbatim as the object's contents,
1981
- * boundary lines and all. `name` and `formData` are then rejected
1982
- * rather than silently dropped, and `fileName` is ignored.
2267
+ * boundary lines and all. `name`, `formData`, and `fileName` are
2268
+ * then rejected rather than silently dropped.
1983
2269
  * @example
1984
2270
  * ```ts
1985
2271
  * // A presigned URL is signed for one method and one Content-Type,
@@ -1991,8 +2277,8 @@ export type UploadIteratorResult = {
1991
2277
  * bodyMode: 'raw',
1992
2278
  * mimeType: 'video/mp4',
1993
2279
  * });
1994
- * for await (const event of task) render(event.progress);
1995
- * const { statusCode } = await task;
2280
+ * for await (const event of task.progress) render(event.progress);
2281
+ * const { statusCode } = await task.result;
1996
2282
  * ```
1997
2283
  */
1998
2284
  export type UploadOptions = {
@@ -2005,14 +2291,6 @@ export type UploadOptions = {
2005
2291
  * A presigned URL is signed for exactly one method, usually `PUT`.
2006
2292
  */
2007
2293
  method?: 'POST' | 'PUT' | 'PATCH';
2008
- /**
2009
- * How the file bytes are framed. Default: `multipart`.
2010
- * `raw` sends them as the whole body under a `Content-Length` taken from
2011
- * the file itself, which is what presigned endpoints require.
2012
- */
2013
- bodyMode?: 'multipart' | 'raw';
2014
- /** Name of the multipart part carrying the file. Default: `file`. Multipart only. */
2015
- name?: string;
2016
2294
  /**
2017
2295
  * Optional request headers.
2018
2296
  * Restricted headers such as `Referer` are ignored by the runtime.
@@ -2021,12 +2299,8 @@ export type UploadOptions = {
2021
2299
  * carries the part boundary.
2022
2300
  */
2023
2301
  headers?: Record<string, string>;
2024
- /** Text fields sent alongside the file in the envelope. Multipart only. */
2025
- formData?: Record<string, string>;
2026
2302
  /** Request timeout in milliseconds. */
2027
- timeout?: number;
2028
- /** Filename announced for the file part. Defaults to the file's own name. Multipart only. */
2029
- fileName?: string;
2303
+ timeoutMs?: number;
2030
2304
  /**
2031
2305
  * File MIME type. Types the file part under `multipart`; becomes the
2032
2306
  * request `Content-Type` under `raw`, where it defaults to
@@ -2035,10 +2309,26 @@ export type UploadOptions = {
2035
2309
  mimeType?: string;
2036
2310
  /** Optional abort signal. */
2037
2311
  signal?: AbortSignal;
2038
- };
2312
+ } & ({
2313
+ /**
2314
+ * How the file bytes are framed. Default: `multipart`.
2315
+ */
2316
+ bodyMode?: 'multipart';
2317
+ /** Name of the multipart part carrying the file. Default: `file`. */
2318
+ name?: string;
2319
+ /** Text fields sent alongside the file. Must be non-empty when set. */
2320
+ formData?: Record<string, string>;
2321
+ /** Filename announced for the file part. Defaults to the file's own name. */
2322
+ fileName?: string;
2323
+ } | {
2324
+ /** Send the file bytes as the whole body. Multipart fields are rejected. */
2325
+ bodyMode: 'raw';
2326
+ name?: never;
2327
+ formData?: never;
2328
+ fileName?: never;
2329
+ });
2039
2330
  export type UploadProgressEvent = {
2040
- /** `completed` and `canceled` are terminal; iteration ends after either. */
2041
- kind: 'progress' | 'canceled' | 'completed';
2331
+ kind: 'progress' | 'canceled';
2042
2332
  /** Bytes handed to the socket so far, envelope included under `multipart`. */
2043
2333
  uploadedBytes?: number;
2044
2334
  /**
@@ -2049,8 +2339,12 @@ export type UploadProgressEvent = {
2049
2339
  totalBytes?: number;
2050
2340
  /** `uploadedBytes / totalBytes`, absent while the total is unknown or zero. */
2051
2341
  progress?: number;
2052
- /** Present on `completed` only. */
2053
- result?: UploadResult;
2342
+ } | {
2343
+ kind: 'completed';
2344
+ uploadedBytes?: number;
2345
+ totalBytes?: number;
2346
+ progress?: number;
2347
+ result: UploadResult;
2054
2348
  };
2055
2349
  export type UploadResult = {
2056
2350
  /** HTTP status code returned by the server. */
@@ -2058,21 +2352,13 @@ export type UploadResult = {
2058
2352
  /** Response body decoded as UTF-8 text. */
2059
2353
  data: string;
2060
2354
  };
2061
- export type UploadTask = PromiseLike<UploadResult> & AsyncIterable<UploadProgressEvent> & {
2062
- next(): Promise<UploadIteratorResult>;
2063
- /** Stops iteration only. Does not cancel the underlying upload task. */
2064
- return(): Promise<UploadIteratorResult>;
2065
- catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<UploadResult | TResult>;
2066
- finally(onfinally?: (() => void) | null): Promise<UploadResult>;
2067
- cancel(): Promise<void>;
2068
- wait(): Promise<UploadResult>;
2069
- };
2355
+ export type UploadTask = CancelableTask<UploadResult, UploadProgressEvent>;
2070
2356
  export type VideoCompressQuality = 'low' | 'medium' | 'high';
2071
2357
  export type VideoContext = {
2072
2358
  play(): void;
2073
2359
  pause(): void;
2074
2360
  stop(): void;
2075
- seek(position: number): void;
2361
+ seek(positionSeconds: number): void;
2076
2362
  requestFullScreen(): void;
2077
2363
  exitFullScreen(): void;
2078
2364
  setStreamSource(options: StreamSourceOptions): void;
@@ -2184,7 +2470,7 @@ export type WindowsTerminalInlineImageStatus = {
2184
2470
  /**
2185
2471
  * Host app identity. Everything here is fixed for the life of the process;
2186
2472
  * the language the app renders in is not, and lives on
2187
- * `lx.app.displayLanguage`.
2473
+ * `lx.host.displayLanguage`.
2188
2474
  */
2189
2475
  export interface AppBaseInfo {
2190
2476
  /**
@@ -2194,7 +2480,7 @@ export interface AppBaseInfo {
2194
2480
  os: HostOs;
2195
2481
  productName: string;
2196
2482
  version: string;
2197
- SDKVersion: string;
2483
+ sdkVersion: string;
2198
2484
  }
2199
2485
  /** Device info APIs. */
2200
2486
  export interface DeviceInfo {
@@ -2256,9 +2542,9 @@ export interface SystemSettingInfo {
2256
2542
  /** Wi-Fi APIs. */
2257
2543
  export interface WifiInfo {
2258
2544
  /** Service Set Identifier (network name) */
2259
- SSID: string;
2545
+ ssid: string;
2260
2546
  /** Basic Service Set Identifier (MAC address) */
2261
- BSSID?: string;
2547
+ bssid?: string;
2262
2548
  /** Whether the network is secure (requires password) */
2263
2549
  secure: boolean;
2264
2550
  /** Signal strength (0-100, higher is better) */
@@ -2266,15 +2552,13 @@ export interface WifiInfo {
2266
2552
  /** Center frequency in MHz (if available) */
2267
2553
  frequency?: number;
2268
2554
  }
2269
- export declare class DirEntry {
2270
- private constructor();
2555
+ export interface DirEntry {
2271
2556
  readonly name: string;
2272
2557
  readonly isFile: boolean;
2273
2558
  readonly isDirectory: boolean;
2274
2559
  readonly isSymlink: boolean;
2275
2560
  }
2276
- export declare class LxFile {
2277
- private constructor();
2561
+ export interface LxFile {
2278
2562
  /** The path supplied to `lx.fs.file`. */
2279
2563
  readonly path: string;
2280
2564
  /** Read the complete file as strict UTF-8 text. */
@@ -2297,6 +2581,56 @@ export declare class LxFile {
2297
2581
  /** Read metadata for this managed path. */
2298
2582
  stat(): Promise<FileStats>;
2299
2583
  }
2584
+ declare global {
2585
+ interface ClipboardApi {
2586
+ /**
2587
+ * Replace the clipboard with Unicode text.
2588
+ * Empty string is a valid payload (it is not `clear()`). The runtime does not
2589
+ * present a toast — call `lx.showToast` if the product wants one. Rejects
2590
+ * `E_INVALID_ARG` above 1 MiB.
2591
+ */
2592
+ writeText(text: string): Promise<void>;
2593
+ /**
2594
+ * Read Unicode text.
2595
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the OS paste
2596
+ * prompt (iOS 16+, macOS 15.4+). No text representation (empty clipboard, or
2597
+ * image-only) resolves `{ status: 'empty' }`. A copied empty
2598
+ * string resolves `{ status: 'ok', text: '' }`.
2599
+ * Rejects `E_PERMISSION_DENIED` when the host denies clipboard access
2600
+ * outright: a macOS "never allow" setting, or HarmonyOS without
2601
+ * `ohos.permission.READ_PASTEBOARD`. Android denies a read while the app has
2602
+ * no window focus and reports it as an empty clipboard, so read in response
2603
+ * to a user action.
2604
+ */
2605
+ readText(): Promise<ClipboardTextResult>;
2606
+ /**
2607
+ * Replace the clipboard with a typed item.
2608
+ * Rejects `E_INVALID_ARG` for text above 1 MiB or an image file that does not
2609
+ * decode.
2610
+ */
2611
+ write(item: ClipboardWriteItem): Promise<void>;
2612
+ /**
2613
+ * Read the clipboard.
2614
+ * Omit `type` to receive every representation this host can surface.
2615
+ * Pass `type` to request one; if that representation is absent, the
2616
+ * completed result is `{ status: 'empty' }` rather than a mismatch error.
2617
+ * Images arrive as a temporary PNG under `lx://temp`. Dismissal and
2618
+ * permission behave as in `readText`.
2619
+ */
2620
+ read(options?: ClipboardReadOptions): Promise<ClipboardReadResult>;
2621
+ /** Remove every representation. */
2622
+ clear(): Promise<void>;
2623
+ /**
2624
+ * Which representations are present, without reading payloads. An empty
2625
+ * array is an empty clipboard; representations this runtime cannot
2626
+ * round-trip (HTML, files) are omitted.
2627
+ * Never shows the OS paste prompt and needs no permission on any host, so it
2628
+ * is the way to decide whether to offer "Paste". The answer is a hint —
2629
+ * content may change before you read it.
2630
+ */
2631
+ types(): Promise<ClipboardType[]>;
2632
+ }
2633
+ }
2300
2634
  declare global {
2301
2635
  interface FileSystemApi {
2302
2636
  /**
@@ -2304,44 +2638,53 @@ declare global {
2304
2638
  * Relative paths resolve under `lx.env.USER_DATA_PATH`. Creating a reference
2305
2639
  * does not require the path to exist.
2306
2640
  */
2307
- file(path: string): LxFile;
2641
+ file(path: ManagedPath): LxFile;
2308
2642
  /** Test whether a managed path currently exists. */
2309
- exists(path: string): Promise<boolean>;
2643
+ exists(path: ManagedPath): Promise<boolean>;
2310
2644
  /** Read metadata for a managed path. */
2311
- stat(path: string): Promise<FileStats>;
2645
+ stat(path: ManagedPath): Promise<FileStats>;
2312
2646
  /** The direct children of a managed directory. */
2313
- readDir(path: string): Promise<DirEntry[]>;
2647
+ readDir(path: ManagedPath): Promise<DirEntry[]>;
2314
2648
  /** Create a managed directory. */
2315
- mkdir(path: string, options?: FsMkdirOptions): Promise<void>;
2649
+ mkdir(path: ManagedPath, options?: FsMkdirOptions): Promise<void>;
2316
2650
  /** Write UTF-8 text or bytes to a managed file. */
2317
- write(path: string, data: string, options?: FsWriteOptions): Promise<void>;
2651
+ write(path: ManagedPath, data: string, options?: FsWriteOptions): Promise<void>;
2318
2652
  /** Copy a managed file. */
2319
- copy(source: string, destination: string, options?: FsCopyOptions): Promise<void>;
2653
+ copy(source: ManagedPath, destination: ManagedPath, options?: FsCopyOptions): Promise<void>;
2320
2654
  /** Rename or move a managed file or directory. */
2321
- rename(source: string, destination: string, options?: FsRenameOptions): Promise<void>;
2655
+ rename(source: ManagedPath, destination: ManagedPath, options?: FsRenameOptions): Promise<void>;
2322
2656
  /** Remove a managed file or directory. */
2323
- remove(path: string, options?: FsRemoveOptions): Promise<void>;
2657
+ remove(path: ManagedPath, options?: FsRemoveOptions): Promise<void>;
2324
2658
  }
2325
2659
  }
2326
2660
  declare global {
2327
2661
  interface HostAppApi {
2328
2662
  /**
2329
- * `lx.app.screenshot(options?)` — capture the host app's window as a PNG.
2663
+ * `lx.host.screenshot(options?)` — capture the host app's window as a PNG.
2330
2664
  * App-level semantics, one level above any page/WebView capture: the image
2331
2665
  * is what the user sees of the whole app — host-drawn navigation chrome,
2332
2666
  * native overlays, and every composited WebView, not just this lxapp's web
2333
2667
  * content. Because that view can include other lxapps' UI, the API is
2334
- * restricted to the Control app, like the other host-level APIs on `lx.app`.
2668
+ * restricted to the Control app, like the other host-level APIs on `lx.host`.
2335
2669
  */
2336
2670
  screenshot(options?: AppScreenshotOptions): Promise<AppScreenshotResult>;
2337
2671
  /**
2338
- * Check whether the host app has an update.
2339
- * This host-level capability is restricted to the Control app. Calling it opts
2340
- * the process into custom update handling. Incompatible updates are hidden as
2341
- * `hasUpdate: false`; platforms that cannot apply a package may still return
2342
- * metadata and reject when `update.apply()` is invoked.
2672
+ * Query whether the host app has an update.
2673
+ * This host-level capability is restricted to the Control app. A successful
2674
+ * check does **not** take over the built-in auto-flow — that is
2675
+ * `claimCustomUpdate()` or `update.apply()`. Permission denial or a failed
2676
+ * check claims nothing. Incompatible updates are hidden as
2677
+ * `hasUpdate: false`. Store-channel hosts still surface a newer feed version;
2678
+ * `apply()` opens the store listing instead of downloading.
2343
2679
  */
2344
2680
  checkUpdate(): Promise<HostAppUpdateCheckResult>;
2681
+ /**
2682
+ * Claim the process-lifetime custom host-update flow.
2683
+ * Irreversible: the built-in auto-flow will not prompt or download again,
2684
+ * including after the calling page unloads. Does not cancel an already-started
2685
+ * update task. Later failed checks do not undo a claim already made.
2686
+ */
2687
+ claimCustomUpdate(): void;
2345
2688
  readonly env: HostAppEnv;
2346
2689
  /**
2347
2690
  * Read the host app's identity: OS, product name, product version, and SDK
@@ -2357,29 +2700,31 @@ declare global {
2357
2700
  */
2358
2701
  exit(): void;
2359
2702
  /**
2360
- * Set the app-icon badge, for example an unread count.
2361
- * This targets the dock on macOS, taskbar on Windows, and home/launcher icon
2362
- * on mobile — the product's own icon, not the calling lxapp's, so it is
2363
- * Control app only and other lxapps get a permission error. Null or an empty
2364
- * string clears it. Unsupported platforms treat the call as a no-op.
2703
+ * Mark the product in system chrome, for example with an unread count.
2704
+ * One call, because "where the count goes" is the platform's answer, not the
2705
+ * caller's: `auto` paints every product-owned surface this platform has — the
2706
+ * dock and the menu-bar item on macOS, the taskbar and the notification-area
2707
+ * item on Windows, the home-screen icon on iOS and HarmonyOS. Name a
2708
+ * `surface` only when one of them is the point.
2709
+ * It is the product's chrome, not the calling lxapp's, so it is Control app
2710
+ * only. Null or an empty string clears it.
2711
+ * Returns whether anything was actually painted. A platform with no such
2712
+ * chrome is a no-op that returns `false` rather than an error — portable code
2713
+ * can call this unconditionally.
2365
2714
  */
2366
- setBadge(value: string | number | null): void;
2715
+ setBadge(value: string | number | null, options?: SetBadgeOptions): Promise<boolean>;
2367
2716
  }
2368
2717
  }
2369
2718
  declare global {
2370
2719
  interface Lx {
2371
- readonly app: HostAppApi;
2720
+ readonly host: HostAppApi;
2372
2721
  /**
2373
- * Whether this host exposes a capability to this Logic context, right now.
2374
- * Synchronous, because it is meant to be called from render paths. The answer
2375
- * is live and may be stale by the time you act on it — it is an affordance for
2376
- * deciding what to render, not a replacement for handling a rejection.
2377
- * `{ capability: 'surface', value: 'aside' }` in particular changes when a
2378
- * desktop window crosses the compact breakpoint; pair it with
2379
- * `lx.surface.onContext` instead of polling. The answer is per runtime context:
2380
- * a context that does not expose an API reports false for it.
2722
+ * Frozen feature support, not permission or current layout. Unknown strings
2723
+ * return false; non-strings throw TypeError. Required features also need an
2724
+ * appropriate lxapp.json minRuntime.
2381
2725
  */
2382
- supports(query: LxCapabilityQuery): boolean;
2726
+ supports(feature: LxFeature): boolean;
2727
+ readonly clipboard: ClipboardApi;
2383
2728
  /** Vibrate briefly, where the device has a vibrator. */
2384
2729
  vibrateShort(): boolean;
2385
2730
  /** Vibrate for a longer pulse, where the device has a vibrator. */
@@ -2426,8 +2771,8 @@ declare global {
2426
2771
  * `method: 'PUT'` with `bodyMode: 'raw'` to send the file bytes as the whole
2427
2772
  * body instead, which is what presigned object-storage URLs expect.
2428
2773
  * Returns the task handle synchronously, before the transfer starts, so
2429
- * progress and cancellation can be wired up without racing it: the handle is
2430
- * awaitable for the final result, async-iterable for progress, and cancelable.
2774
+ * progress and cancellation can be wired up without racing it: `result` settles
2775
+ * once, `progress` streams to one consumer, and `cancel()` aborts.
2431
2776
  */
2432
2777
  uploadFile(options: UploadOptions): UploadTask;
2433
2778
  /**
@@ -2436,17 +2781,20 @@ declare global {
2436
2781
  * `mode: "auto"`.
2437
2782
  */
2438
2783
  openFile(options: OpenFileOptions): Promise<void>;
2784
+ /** Pick one file. A successful selection returns one opaque URI. */
2785
+ pickFile(options?: PickFileOptions): Promise<PickFileResult>;
2786
+ /** Pick one or more files. Dismissal is a normal outcome. */
2787
+ pickFiles(options?: PickFileOptions): Promise<ChooseFileResult>;
2439
2788
  /**
2440
- * Opens a file picker.
2441
- * Resolves `{ canceled: true }` only when the user dismisses the picker. A
2442
- * completed selection resolves `{ canceled: false, paths }` with at least one
2789
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the picker. A
2790
+ * completed selection resolves `{ status: 'ok', paths }` with at least one
2443
2791
  * path. Rejects when the picker fails or returns an invalid payload.
2444
2792
  */
2445
- chooseFile(options?: ChooseFileOptions): Promise<ChooseFileResult>;
2793
+ chooseFile(options: never): Promise<never>;
2446
2794
  /**
2447
2795
  * Opens a directory picker.
2448
- * Resolves `{ canceled: true }` only when the user dismisses the picker. A
2449
- * completed selection resolves `{ canceled: false, path }`. Rejects when the
2796
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the picker. A
2797
+ * completed selection resolves `{ status: 'ok', path }`. Rejects when the
2450
2798
  * picker fails or returns an invalid payload.
2451
2799
  */
2452
2800
  chooseDirectory(options?: ChooseDirectoryOptions): Promise<ChooseDirectoryResult>;
@@ -2465,8 +2813,8 @@ declare global {
2465
2813
  compressImage(options: CompressImageOptions): Promise<CompressImageResult>;
2466
2814
  /**
2467
2815
  * Opens the media picker or camera.
2468
- * Resolves `{ canceled: true }` only when the user dismisses the picker. A
2469
- * completed selection resolves `{ canceled: false, entries }` with at least one
2816
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the picker. A
2817
+ * completed selection resolves `{ status: 'ok', entries }` with at least one
2470
2818
  * entry. Rejects when capture or selection fails, or the host returns an invalid
2471
2819
  * payload.
2472
2820
  */
@@ -2474,10 +2822,8 @@ declare global {
2474
2822
  /**
2475
2823
  * Synchronously returns a JS handle so listeners can be attached before the
2476
2824
  * first event fires:
2477
- * - `presented`: Promise, resolves with no value when the first pixel of the
2478
- * underlying media is composited to screen. Also resolves unconditionally
2479
- * once `completed` settles, so consumers can safely ignore it (it never
2480
- * rejects).
2825
+ * - `presented`: resolves `{ status: 'presented' }` only after the first
2826
+ * frame; otherwise `{ status: 'notPresented', reason }`. Never rejects.
2481
2827
  * - `current`: `{ index, source }` snapshot of the item on screen, updated
2482
2828
  * live as the user swipes / the session auto-advances.
2483
2829
  * - `onChange(listener)`: fires `{ index, source }` for every item change
@@ -2495,8 +2841,8 @@ declare global {
2495
2841
  saveVideoToPhotosAlbum(options: SaveMediaOptions): Promise<void>;
2496
2842
  /**
2497
2843
  * Opens the scanner.
2498
- * Resolves `{ canceled: true }` only when the user dismisses the scanner. A
2499
- * completed scan resolves `{ canceled: false, scanResult, scanType }`. Rejects
2844
+ * Resolves `{ status: 'canceled' }` only when the user dismisses the scanner. A
2845
+ * completed scan resolves `{ status: 'ok', scanResult, scanType }`. Rejects
2500
2846
  * when scanning fails or the host returns an invalid payload.
2501
2847
  */
2502
2848
  scanCode(options?: ScanCodeOptions): Promise<ScanCodeResult>;
@@ -2546,15 +2892,18 @@ declare global {
2546
2892
  getSystemSetting(): SystemSettingInfo;
2547
2893
  /**
2548
2894
  * Shows a list of actions.
2549
- * Resolves `{ canceled: false, index }` when the user selects an item; `index`
2550
- * points into `options.itemList`. Resolves `{ canceled: true }` only when the
2895
+ * Resolves `{ status: 'ok', id }` when the user selects an item; `id` identifies the selected item. Resolves `{ status: 'canceled' }` only when the
2551
2896
  * user dismisses the sheet. Rejects when presentation fails or the host returns
2552
2897
  * an invalid selection.
2553
2898
  */
2554
2899
  showActionSheet(options: ShowActionSheetOptions): Promise<ActionSheetResult>;
2900
+ /** Acknowledgement-only dialog. Resolves when the user dismisses it. */
2901
+ alert(options: AlertOptions): Promise<void>;
2902
+ /** Ask a yes/no question. Dismissal resolves false; presentation failure rejects. */
2903
+ confirm(options: ConfirmOptions): Promise<boolean>;
2555
2904
  /**
2556
2905
  * Shows a confirmation modal.
2557
- * Resolves `{ canceled: false }` when the user confirms and `{ canceled: true }`
2906
+ * Resolves `{ status: 'ok' }` when the user confirms and `{ status: 'canceled' }`
2558
2907
  * only when the user dismisses or cancels the modal. Rejects when presentation
2559
2908
  * fails or the host returns an invalid payload.
2560
2909
  */
@@ -2611,15 +2960,19 @@ declare global {
2611
2960
  reLaunch(options: ReLaunchOptions): Promise<void>;
2612
2961
  readonly shell: ShellApi;
2613
2962
  readonly tabBar: TabBarApi;
2614
- /** Show toast function */
2615
- showToast(options: ShowToastOptions): Promise<void>;
2616
- /** Hide toast function */
2963
+ /**
2964
+ * Presents a toast and resolves a handle once the host accepted it. The handle
2965
+ * dismisses only this toast, never a newer one — including a newer one posted
2966
+ * by another lxapp onto a host's shared overlay.
2967
+ */
2968
+ showToast(options: ShowToastOptions): Promise<ToastHandle>;
2969
+ /** Hides whichever toast is showing. */
2617
2970
  hideToast(): Promise<void>;
2618
2971
  readonly tray: TrayApi;
2619
2972
  /**
2620
2973
  * Return the callback-based update manager for this lxapp's bundle. This is
2621
2974
  * available to every lxapp and is distinct from the Control-app-only
2622
- * `lx.app.checkUpdate()`, which updates the native host app.
2975
+ * `lx.host.checkUpdate()`, which updates the native host app.
2623
2976
  */
2624
2977
  getUpdateManager(): UpdateManager;
2625
2978
  }
@@ -2708,34 +3061,35 @@ declare global {
2708
3061
  * `lingxia.yaml`, opened with the declaration's own presentation.
2709
3062
  */
2710
3063
  openDeclared(id: string): Promise<DeclaredSurface>;
3064
+ /** Find a live surface by its explicit caller-owned key. Runtime ids are not keys. */
3065
+ getByKey(key: string): AnySurface | undefined;
2711
3066
  /**
2712
- * `lx.surface.get(keyOrId)` — the live handle for a surface this lxapp opened
2713
- * **with a `key`**, so no caller has to cache one in order to reuse or close
2714
- * it. An unkeyed surface is not addressable: nothing registers it, because
2715
- * holding one for the session costs its closures and its message port and
2716
- * nobody can look up a uuid they never chose.
2717
- * A `key` you chose wins over a runtime-assigned `id`, so a key that happens
2718
- * to spell another surface's id still finds yours.
2719
- */
2720
- get(keyOrId: string): AnySurface | undefined;
2721
- /**
2722
- * `lx.surface.onContext(handler)` — register a JS callback (scoped to this
3067
+ * `lx.surface.watchContext(handler)` — register a JS callback (scoped to this
2723
3068
  * lxapp's JS context), invoke it immediately, then again whenever that
2724
- * presentation's actual viewport changes. Returns an unsubscribe fn.
3069
+ * presentation's viewport or host docking availability changes. Returns an unsubscribe fn.
2725
3070
  */
2726
- onContext(handler: (context: SurfaceContext) => void): () => void;
3071
+ watchContext(handler: (context: SurfaceContext) => void): () => void;
2727
3072
  }
2728
3073
  }
2729
3074
  declare global {
2730
3075
  interface TabBarApi {
2731
- /** Patch this lxapp's tab bar; unset fields stay as they are. */
3076
+ /**
3077
+ * Patch this lxapp's tab bar; unset fields stay as they are.
3078
+ * Items, badges, red dots, and visibility only. A `style` field is
3079
+ * rejected — colors stay in static `lxapp.json` `tabBar.style`.
3080
+ * `tabBar.style.backgroundColor` is mobile-only: it paints the bar on
3081
+ * iOS / Android / Harmony. On macOS / Windows the sidebar follows the
3082
+ * host `lingxia.yaml` theme (`windowBackgroundColor`) instead, so a
3083
+ * `#FFFFFF` fill cannot paint a card on the sidebar. Other style keys
3084
+ * (`foregroundColor`, `selectedForegroundColor`, `dividerColor`) may
3085
+ * tint items while the desktop host is light; a dark host uses the
3086
+ * shell theme for every key.
3087
+ */
2732
3088
  update(patch: TabBarPatch): Promise<void>;
2733
3089
  }
2734
3090
  }
2735
3091
  declare global {
2736
3092
  interface TrayApi {
2737
- /** lx.tray.setBadge(value) — the menu-bar / system-tray badge. Null/empty clears it. */
2738
- setBadge(value: string | number | null): void;
2739
3093
  /** lx.tray.setIcon(icon) — replace the tray icon (a resource path). */
2740
3094
  setIcon(icon: string): void;
2741
3095
  /** lx.tray.setTitle(text) — text shown beside the icon (macOS). Empty clears it. */
@@ -2760,4 +3114,6 @@ declare global {
2760
3114
  }
2761
3115
  }
2762
3116
  export {};
3117
+ /** Feature contracts generated from the runtime registry. */
3118
+ 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';
2763
3119
  //# sourceMappingURL=logic.d.ts.map