@linxin666/dsh-remote-web-ui 0.1.13 → 0.1.15

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 (38) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +5 -2
  3. package/README.zh.md +1 -1
  4. package/lib/client.js +30 -17
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +177 -34
  7. package/lib/mobile.js +44 -25
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.d.ts +26 -11
  10. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  11. package/lib/types/client/PluginSettingsCard.js +27 -7
  12. package/lib/types/client/locales.js +2 -2
  13. package/lib/types/client/settings-form.d.ts +14 -5
  14. package/lib/types/client/settings-form.d.ts.map +1 -1
  15. package/lib/types/client/settings-form.js +38 -10
  16. package/lib/types/mobile/mux.d.ts +13 -4
  17. package/lib/types/mobile/mux.d.ts.map +1 -1
  18. package/lib/types/mobile/mux.js +50 -30
  19. package/lib/types/mobile/mux.test.js +75 -6
  20. package/lib/types/mobile-api.d.ts +2 -0
  21. package/lib/types/mobile-api.d.ts.map +1 -1
  22. package/lib/types/mobile-api.js +4 -1
  23. package/lib/types/routes.d.ts +17 -0
  24. package/lib/types/routes.d.ts.map +1 -1
  25. package/lib/types/routes.js +49 -10
  26. package/lib/types/update.d.ts +5 -1
  27. package/lib/types/update.d.ts.map +1 -1
  28. package/lib/types/update.js +122 -26
  29. package/package.json +1 -1
  30. package/src/client/PluginSettingsCard.tsx +83 -17
  31. package/src/client/locales.ts +2 -2
  32. package/src/client/settings-card.module.css +6 -0
  33. package/src/client/settings-form.ts +41 -9
  34. package/src/mobile/mux.test.ts +84 -6
  35. package/src/mobile/mux.ts +46 -25
  36. package/src/mobile-api.ts +6 -1
  37. package/src/routes.ts +51 -10
  38. package/src/update.ts +116 -26
@@ -1,22 +1,27 @@
1
1
  /**
2
- * Shared chrome for the plugin settings card: a disclosure header naming the
3
- * plugin and what its settings govern, the controls inside, and the save that
4
- * writes them. Renders nothing while the namespace is unavailable — a
2
+ * Family-shared chrome for plugin settings cards: a disclosure header naming
3
+ * the plugin and what its settings govern, the controls inside, and the save
4
+ * that writes them. Renders nothing while the namespace is unavailable — a
5
5
  * deployment that does not compose the owning plugin should show no trace of
6
- * it. Mirrors the official ui-plugin-config PluginCard in a self-contained
7
- * slice (this package must not depend on a sibling UI package).
6
+ * it. Inlined into each consumer's client bundle; mirrors the official
7
+ * ui-plugin-config PluginCard in a self-contained slice.
8
8
  */
9
9
  import { type ReactNode } from 'react';
10
10
  import type { CardShell } from './settings-form.ts';
11
- import type { SettingsCardKey } from './locales.ts';
11
+ /** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
12
+ export declare const CARD_COPY_KEYS: readonly ["settings.collapse", "settings.expand", "settings.notExposed", "settings.unsaved", "settings.readOnly", "settings.saveFailed", "settings.discard", "settings.save", "settings.saving"];
13
+ /** Copy key the card chrome itself reads. */
14
+ export type CardCopyKey = (typeof CARD_COPY_KEYS)[number];
15
+ /** Key domain for the plugin's own copy (inferred per consumer). */
16
+ export type SettingsCardKey<TKey extends string = string> = TKey | CardCopyKey;
12
17
  /** Card chrome shared by every plugin settings card. */
13
- export interface PluginSettingsCardProps {
18
+ export interface PluginSettingsCardProps<TKey extends string = string> {
14
19
  /** Locale reader for this card's copy. */
15
- t: (key: SettingsCardKey) => string;
20
+ t: (key: SettingsCardKey<TKey>, params?: Record<string, string | number>) => string;
16
21
  /** Locale key of the plugin's name. */
17
- titleKey: SettingsCardKey;
22
+ titleKey: TKey;
18
23
  /** Locale key of the line describing what this plugin's settings govern. */
19
- descriptionKey: SettingsCardKey;
24
+ descriptionKey: TKey;
20
25
  /** The card's form state: availability, writability, and what a save would do. */
21
26
  state: CardShell;
22
27
  /** Write every staged edit. */
@@ -31,7 +36,7 @@ export interface PluginSettingsCardProps {
31
36
  * @param props - the plugin's copy keys, its form state, and its controls.
32
37
  * @returns the card, or nothing while the namespace is still loading.
33
38
  */
34
- export declare function PluginSettingsCard(props: PluginSettingsCardProps): import("react").JSX.Element | null;
39
+ export declare function PluginSettingsCard<TKey extends string = string>(props: PluginSettingsCardProps<TKey>): import("react").JSX.Element | null;
35
40
  /** Props every field control needs regardless of its value type. */
36
41
  export interface FieldProps {
37
42
  /** Stable id associating the label with its control. */
@@ -75,4 +80,14 @@ export declare function BooleanField(props: FieldProps & {
75
80
  /** Copy for the off option. */
76
81
  offLabel: string;
77
82
  }): import("react").JSX.Element;
83
+ /** A staged enumerated field rendered as a select. */
84
+ export declare function ChoiceField(props: FieldProps & {
85
+ /** Copy for the inherit option (draft text is the empty string). */
86
+ inheritLabel: string;
87
+ /** Choices rendered in order; `value` is the draft/stored text. */
88
+ choices: ReadonlyArray<{
89
+ value: string;
90
+ label: string;
91
+ }>;
92
+ }): import("react").JSX.Element;
78
93
  //# sourceMappingURL=PluginSettingsCard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAGnD,wDAAwD;AACxD,MAAM,WAAW,uBAAuB;IACtC,0CAA0C;IAC1C,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,MAAM,CAAA;IACnC,uCAAuC;IACvC,QAAQ,EAAE,eAAe,CAAA;IACzB,4EAA4E;IAC5E,cAAc,EAAE,eAAe,CAAA;IAC/B,kFAAkF;IAClF,KAAK,EAAE,SAAS,CAAA;IAChB,+BAA+B;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,6BAA6B;IAC7B,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,sCA0GhE;AAED,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAA;IACV,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,UAAU,EAAE,OAAO,CAAA;IACnB,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAA;IAChB,qCAAqC;IACrC,eAAe,EAAE,MAAM,CAAA;IACvB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAA;IACjB,wBAAwB;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,oEAAoE;IACpE,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,kHAAkH;AAClH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG;IAC7C,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,+BAqCA;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG;IAC/C,mCAAmC;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,+BAmCA"}
1
+ {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAGnD,oGAAoG;AACpG,eAAO,MAAM,cAAc,kMAUjB,CAAA;AAEV,6CAA6C;AAC7C,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,oEAAoE;AACpE,MAAM,MAAM,eAAe,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI,IAAI,GAAG,WAAW,CAAA;AAE9E,wDAAwD;AACxD,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACnE,0CAA0C;IAC1C,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAA;IACnF,uCAAuC;IACvC,QAAQ,EAAE,IAAI,CAAA;IACd,4EAA4E;IAC5E,cAAc,EAAE,IAAI,CAAA;IACpB,kFAAkF;IAClF,KAAK,EAAE,SAAS,CAAA;IAChB,+BAA+B;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,6BAA6B;IAC7B,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,uBAAuB,CAAC,IAAI,CAAC,sCA2GpG;AAED,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAA;IACV,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,UAAU,EAAE,OAAO,CAAA;IACnB,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAA;IAChB,qCAAqC;IACrC,eAAe,EAAE,MAAM,CAAA;IACvB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAA;IACjB,wBAAwB;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,oEAAoE;IACpE,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,kHAAkH;AAClH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG;IAC7C,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,+BAqCA;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG;IAC/C,mCAAmC;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,+BAmCA;AAED,sDAAsD;AACtD,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG;IAC9C,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,OAAO,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACzD,+BAsCA"}
@@ -1,14 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Generated by scripts/sync-shared.mjs from shared/client/settings/PluginSettingsCard.tsx. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
3
  /**
3
- * Shared chrome for the plugin settings card: a disclosure header naming the
4
- * plugin and what its settings govern, the controls inside, and the save that
5
- * writes them. Renders nothing while the namespace is unavailable — a
4
+ * Family-shared chrome for plugin settings cards: a disclosure header naming
5
+ * the plugin and what its settings govern, the controls inside, and the save
6
+ * that writes them. Renders nothing while the namespace is unavailable — a
6
7
  * deployment that does not compose the owning plugin should show no trace of
7
- * it. Mirrors the official ui-plugin-config PluginCard in a self-contained
8
- * slice (this package must not depend on a sibling UI package).
8
+ * it. Inlined into each consumer's client bundle; mirrors the official
9
+ * ui-plugin-config PluginCard in a self-contained slice.
9
10
  */
10
11
  import { useState } from 'react';
11
12
  import css from './settings-card.module.css';
13
+ /** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
14
+ export const CARD_COPY_KEYS = [
15
+ 'settings.collapse',
16
+ 'settings.expand',
17
+ 'settings.notExposed',
18
+ 'settings.unsaved',
19
+ 'settings.readOnly',
20
+ 'settings.saveFailed',
21
+ 'settings.discard',
22
+ 'settings.save',
23
+ 'settings.saving',
24
+ ];
12
25
  /**
13
26
  * Render one plugin settings card.
14
27
  * @param props - the plugin's copy keys, its form state, and its controls.
@@ -20,6 +33,7 @@ export function PluginSettingsCard(props) {
20
33
  if (!state.available)
21
34
  return null;
22
35
  const title = props.t(props.titleKey);
36
+ const description = props.t(props.descriptionKey);
23
37
  const blocked = !state.dirty || state.invalid || state.saving;
24
38
  const cardClass = open ? `${css.cardOpen} ${css.card}` : css.card;
25
39
  // The namespace exists but the Host does not serve it to this client (the
@@ -27,11 +41,11 @@ export function PluginSettingsCard(props) {
27
41
  // that explains the gap instead of vanishing, so a missing card never
28
42
  // reads as a missing plugin.
29
43
  if (!state.exposed) {
30
- return (_jsxs("li", { className: cardClass, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, children: title }), _jsx("span", { className: css.description, children: props.t(props.descriptionKey) })] }), _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron, children: _jsx("path", { d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z", fill: "currentColor" }) })] }), open
44
+ return (_jsxs("li", { className: cardClass, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, title: title, children: title }), _jsx("span", { className: css.description, title: description, children: description })] }), _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron, children: _jsx("path", { d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z", fill: "currentColor" }) })] }), open
31
45
  ? (_jsx("div", { className: css.body, children: _jsx("p", { className: css.notExposed, role: "status", children: props.t('settings.notExposed') }) }))
32
46
  : null] }));
33
47
  }
34
- return (_jsxs("li", { className: cardClass, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, children: title }), _jsx("span", { className: css.description, children: props.t(props.descriptionKey) })] }), state.dirty ? _jsx("span", { className: css.pending, children: props.t('settings.unsaved') }) : null, _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron, children: _jsx("path", { d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z", fill: "currentColor" }) })] }), open
48
+ return (_jsxs("li", { className: cardClass, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, title: title, children: title }), _jsx("span", { className: css.description, title: description, children: description })] }), state.dirty ? _jsx("span", { className: css.pending, title: props.t('settings.unsaved'), children: props.t('settings.unsaved') }) : null, _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron, children: _jsx("path", { d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z", fill: "currentColor" }) })] }), open
35
49
  ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: props.t('settings.readOnly') }) : null, props.children, _jsxs("div", { className: css.footer, children: [state.failed ? _jsx("p", { className: css.failed, role: "status", children: props.t('settings.saveFailed') }) : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.onDiscard, children: props.t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.onSave, children: props.t(!state.saving ? 'settings.save' : 'settings.saving') })] })] }))
36
50
  : null] }));
37
51
  }
@@ -47,3 +61,9 @@ export function BooleanField(props) {
47
61
  ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
48
62
  : null] }), _jsxs("select", { id: props.id, className: css.select, value: props.text, disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); }, children: [_jsx("option", { value: "", children: props.inheritLabel }), _jsx("option", { value: "true", children: props.onLabel }), _jsx("option", { value: "false", children: props.offLabel })] }), _jsx("p", { className: css.hint, children: props.hint })] }));
49
63
  }
64
+ /** A staged enumerated field rendered as a select. */
65
+ export function ChoiceField(props) {
66
+ return (_jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, htmlFor: props.id, children: props.label }), props.overridden
67
+ ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
68
+ : null] }), _jsxs("select", { id: props.id, className: css.select, value: props.text, disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); }, children: [_jsx("option", { value: "", children: props.inheritLabel }), props.choices.map(choice => (_jsx("option", { value: choice.value, children: choice.label }, choice.value)))] }), _jsx("p", { className: props.invalid ? css.invalid : css.hint, children: props.invalid ? props.invalidLabel : props.hint })] }));
69
+ }
@@ -96,7 +96,7 @@ export const zh = {
96
96
  'update.packages': '组件版本',
97
97
  'update.retry': '重新检查',
98
98
  'update.close': '关闭更新面板',
99
- 'update.error.pnpmMissing': '未找到 pnpm,请先安装 pnpm(brew install pnpm 或 corepack enable)。',
99
+ 'update.error.pnpmMissing': '未找到 pnpm,已尝试 pnpm / corepack / npx 仍不可用。请安装 pnpm 后重启应用(Windows: npm install -g pnpm 或 corepack enable;macOS: brew install pnpm 或 corepack enable)。',
100
100
  'update.error.timeout': '更新超时,已终止安装进程,请重试。',
101
101
  'update.error.notFound': '未找到 dsh-web-ui 聚合包安装,无法更新。',
102
102
  'update.error.link': '当前为本地开发模式(链接安装),无法自动更新。',
@@ -200,7 +200,7 @@ export const en = {
200
200
  'update.packages': 'Component versions',
201
201
  'update.retry': 'Check again',
202
202
  'update.close': 'Close update panel',
203
- 'update.error.pnpmMissing': 'pnpm was not found. Install it first (brew install pnpm or corepack enable).',
203
+ 'update.error.pnpmMissing': 'pnpm was not found (tried pnpm / corepack / npx). Install pnpm and restart the app (Windows: npm install -g pnpm or corepack enable; macOS: brew install pnpm or corepack enable).',
204
204
  'update.error.timeout': 'The update timed out and the install process was killed; try again.',
205
205
  'update.error.notFound': 'The dsh-web-ui aggregate package is not installed; nothing to update.',
206
206
  'update.error.link': 'Local development mode (link install) — automatic update is unavailable.',
@@ -2,9 +2,9 @@
2
2
  * Staged form model behind the plugin settings card. A card stages what the
3
3
  * user types and writes it only when they save — the settings write is a
4
4
  * durable, revision-fenced document mutation, so staging keeps what is on
5
- * screen exactly what a save would store. Mirrors the official
6
- * ui-plugin-config card-store pattern in a self-contained slice: this
7
- * package must not depend on a sibling UI package.
5
+ * screen exactly what a save would store. Family-shared slice inlined into
6
+ * each plugin's client bundle; mirrors the official ui-plugin-config
7
+ * card-store pattern.
8
8
  */
9
9
  import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
10
10
  /** The write one field's staged text performs when the card is saved. */
@@ -69,12 +69,21 @@ export interface CardActions {
69
69
  /** Drop every staged edit. */
70
70
  discard: () => void;
71
71
  }
72
- /** A whole-number field. An empty draft clears the field; any other draft that is not a finite number blocks the save. */
73
- export declare function numberField(field: string): FieldSpec;
72
+ /** Constraints a numeric field's accepted drafts must satisfy, mirroring the host schema. */
73
+ export interface NumberConstraints {
74
+ /** The accepted value must be a whole number. */
75
+ integer?: boolean;
76
+ /** The accepted value must be at least this. */
77
+ min?: number;
78
+ }
79
+ /** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
80
+ export declare function numberField(field: string, constraints?: NumberConstraints): FieldSpec;
74
81
  /** A free-text field. An empty draft clears the field. */
75
82
  export declare function textField(field: string): FieldSpec;
76
83
  /** A boolean field, edited through true/false draft text. */
77
84
  export declare function booleanField(field: string): FieldSpec;
85
+ /** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
86
+ export declare function choiceField(field: string, choices: readonly string[]): FieldSpec;
78
87
  /**
79
88
  * Stages one card's edits over one settings namespace and writes them on save.
80
89
  *
@@ -1 +1 @@
1
- {"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"settings-form.d.ts","sourceRoot":"","sources":["../../../src/client/settings-form.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,16 @@
1
+ // Generated by scripts/sync-shared.mjs from shared/client/settings/settings-form.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
1
2
  /**
2
3
  * Staged form model behind the plugin settings card. A card stages what the
3
4
  * user types and writes it only when they save — the settings write is a
4
5
  * durable, revision-fenced document mutation, so staging keeps what is on
5
- * screen exactly what a save would store. Mirrors the official
6
- * ui-plugin-config card-store pattern in a self-contained slice: this
7
- * package must not depend on a sibling UI package.
6
+ * screen exactly what a save would store. Family-shared slice inlined into
7
+ * each plugin's client bundle; mirrors the official ui-plugin-config
8
+ * card-store pattern.
8
9
  */
9
10
  import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
10
- /** A whole-number field. An empty draft clears the field; any other draft that is not a finite number blocks the save. */
11
- export function numberField(field) {
11
+ /** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
12
+ export function numberField(field, constraints = {}) {
13
+ const { integer = false, min } = constraints;
12
14
  return {
13
15
  field,
14
16
  format: value => typeof value === 'number' ? String(value) : '',
@@ -17,7 +19,13 @@ export function numberField(field) {
17
19
  if (trimmed === '')
18
20
  return { kind: 'clear' };
19
21
  const parsed = Number(trimmed);
20
- return Number.isFinite(parsed) ? { kind: 'set', value: parsed } : undefined;
22
+ if (!Number.isFinite(parsed))
23
+ return undefined;
24
+ if (integer && !Number.isInteger(parsed))
25
+ return undefined;
26
+ if (min !== undefined && parsed < min)
27
+ return undefined;
28
+ return { kind: 'set', value: parsed };
21
29
  },
22
30
  };
23
31
  }
@@ -38,14 +46,29 @@ export function booleanField(field) {
38
46
  field,
39
47
  format: value => typeof value === 'boolean' ? String(value) : '',
40
48
  parse: (text) => {
41
- if (text === 'true')
49
+ const trimmed = text.trim();
50
+ if (trimmed === '')
51
+ return { kind: 'clear' };
52
+ if (trimmed === 'true')
42
53
  return { kind: 'set', value: true };
43
- if (text === 'false')
54
+ if (trimmed === 'false')
44
55
  return { kind: 'set', value: false };
45
56
  return undefined;
46
57
  },
47
58
  };
48
59
  }
60
+ /** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
61
+ export function choiceField(field, choices) {
62
+ return {
63
+ field,
64
+ format: value => typeof value === 'string' && choices.includes(value) ? value : '',
65
+ parse: (text) => {
66
+ if (text === '')
67
+ return { kind: 'clear' };
68
+ return choices.includes(text) ? { kind: 'set', value: text } : undefined;
69
+ },
70
+ };
71
+ }
49
72
  /**
50
73
  * Stages one card's edits over one settings namespace and writes them on save.
51
74
  *
@@ -127,6 +150,9 @@ export class CardForm {
127
150
  const writes = plan.flatMap(item => item.run === undefined ? [] : [item.run]);
128
151
  if (plan.length === 0 || this.saving || writes.length !== plan.length)
129
152
  return;
153
+ // Snapshot the fields this save writes, so edits staged while it is in
154
+ // flight survive: only the staged keys this save actually wrote are cleared.
155
+ const fields = new Set(plan.map(item => item.field));
130
156
  this.saving = true;
131
157
  this.failed = false;
132
158
  this.publish();
@@ -134,8 +160,10 @@ export class CardForm {
134
160
  for (const write of writes) {
135
161
  landed = await write() && landed;
136
162
  }
137
- if (landed)
138
- this.staged.clear();
163
+ if (landed) {
164
+ for (const field of fields)
165
+ this.staged.delete(field);
166
+ }
139
167
  this.saving = false;
140
168
  this.failed = !landed;
141
169
  this.publish();
@@ -78,9 +78,11 @@ export declare class MuxClient {
78
78
  private sseAlive;
79
79
  /** Per-session highest event seq already emitted, for poll dedup. */
80
80
  private readonly pollWatermark;
81
- private stallTimer;
82
- private pollTimer;
81
+ /** Single scheduler tick: both the stall check and the poll cadence ride this one interval. */
82
+ private tickTimer;
83
83
  private polling;
84
+ /** Epoch ms of the next due poll while polling (kept on the same tick timer). */
85
+ private nextPollAt;
84
86
  /**
85
87
  * @param url - the mobile events endpoint (browser-relative).
86
88
  * @param options - seams.
@@ -99,8 +101,15 @@ export declare class MuxClient {
99
101
  */
100
102
  observe(sessionId: string | undefined): void;
101
103
  private connect;
102
- private startStallChecker;
103
- private stopStallChecker;
104
+ /**
105
+ * The single scheduler tick is the only interval this client owns. One
106
+ * timer (at the finer of the stall-check and poll cadences) both arms the
107
+ * polling fallback once the stall threshold passes and drives each poll at
108
+ * {@link MuxClientOptions.pollIntervalMs} — one timer instead of two.
109
+ */
110
+ private startTick;
111
+ private stopTick;
112
+ private tick;
104
113
  private startPolling;
105
114
  private stopPolling;
106
115
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACrD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1C,KAAK,IAAI,IAAI,CAAA;CACd;AAiBD;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;IACxE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,wEAAwE;IACxE,OAAO,CAAC,gBAAgB,CAAoB;IAC5C,6EAA6E;IAC7E,OAAO,CAAC,UAAU,CAAI;IACtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAQ;IACxB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,OAAO,CAAC,UAAU,CAA4C;IAC9D,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAQ;IAEvB;;;OAGG;gBACS,GAAG,SAAsB,EAAE,OAAO,GAAE,gBAAqB;IASrE,+EAA+E;IAC/E,KAAK,IAAI,IAAI;IAOb,sBAAsB;IACtB,IAAI,IAAI,IAAI;IAQZ,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAY5C,OAAO,CAAC,OAAO;IAqBf,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;IAQnB;;;;OAIG;YACW,QAAQ;IAsBtB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,IAAI;IAUZ,OAAO,CAAC,WAAW;CAapB"}
1
+ {"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACrD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1C,KAAK,IAAI,IAAI,CAAA;CACd;AAuBD;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;IACxE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,wEAAwE;IACxE,OAAO,CAAC,gBAAgB,CAAoB;IAC5C,6EAA6E;IAC7E,OAAO,CAAC,UAAU,CAAI;IACtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAQ;IACxB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,+FAA+F;IAC/F,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAQ;IACvB,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAI;IAEtB;;;OAGG;gBACS,GAAG,SAAsB,EAAE,OAAO,GAAE,gBAAqB;IASrE,+EAA+E;IAC/E,KAAK,IAAI,IAAI;IAOb,sBAAsB;IACtB,IAAI,IAAI,IAAI;IASZ,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAY5C,OAAO,CAAC,OAAO;IAqBf;;;;;OAKG;IACH,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,IAAI;IAkBZ,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;IAInB;;;;OAIG;YACW,QAAQ;IAsBtB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,IAAI;IAUZ,OAAO,CAAC,WAAW;CAapB"}
@@ -34,6 +34,12 @@ function browserSource(url) {
34
34
  }
35
35
  const DEFAULT_POLL_INTERVAL_MS = 3000;
36
36
  const DEFAULT_STALL_THRESHOLD_MS = 12000;
37
+ /**
38
+ * Stall-check granularity: the single scheduler tick runs at least this
39
+ * often so fallback arms within a second of the stall threshold passing,
40
+ * while the poll cadence itself stays {@link MuxClientOptions.pollIntervalMs}.
41
+ */
42
+ const STALL_CHECK_MS = 1000;
37
43
  /** Poll window: enough recent events to cover a few seconds of agent output. */
38
44
  const DEFAULT_POLL_PAGE_SIZE = 50;
39
45
  /**
@@ -64,9 +70,11 @@ export class MuxClient {
64
70
  sseAlive = false;
65
71
  /** Per-session highest event seq already emitted, for poll dedup. */
66
72
  pollWatermark = new Map();
67
- stallTimer;
68
- pollTimer;
73
+ /** Single scheduler tick: both the stall check and the poll cadence ride this one interval. */
74
+ tickTimer;
69
75
  polling = false;
76
+ /** Epoch ms of the next due poll while polling (kept on the same tick timer). */
77
+ nextPollAt = 0;
70
78
  /**
71
79
  * @param url - the mobile events endpoint (browser-relative).
72
80
  * @param options - seams.
@@ -85,15 +93,16 @@ export class MuxClient {
85
93
  this.lastDataAt = this.now();
86
94
  if (this.source === undefined)
87
95
  this.connect();
88
- this.startStallChecker();
96
+ this.startTick();
89
97
  }
90
98
  /** Close for good. */
91
99
  stop() {
92
100
  this.stopped = true;
93
- this.stopStallChecker();
101
+ this.stopTick();
94
102
  this.stopPolling();
95
103
  this.closeSource();
96
104
  this.observeSessionId = undefined;
105
+ this.nextPollAt = 0;
97
106
  }
98
107
  /** Subscribe to validated frames; returns an unsubscribe function. */
99
108
  onFrame(listener) {
@@ -137,43 +146,54 @@ export class MuxClient {
137
146
  this.startPolling();
138
147
  };
139
148
  }
140
- startStallChecker() {
141
- this.stopStallChecker();
142
- this.stallTimer = setInterval(() => {
143
- if (this.stopped)
144
- return;
145
- if (this.observeSessionId === undefined)
146
- return;
147
- if (this.polling)
148
- return;
149
- // A live SSE channel only goes silent while the agent idles; never
150
- // poll against it. Fallback arms again only via onerror or a stream
151
- // that has never delivered.
152
- if (this.sseAlive)
153
- return;
154
- if ((this.now() - this.lastDataAt) > this.stallThresholdMs)
155
- this.startPolling();
156
- }, 1000);
149
+ /**
150
+ * The single scheduler tick is the only interval this client owns. One
151
+ * timer (at the finer of the stall-check and poll cadences) both arms the
152
+ * polling fallback once the stall threshold passes and drives each poll at
153
+ * {@link MuxClientOptions.pollIntervalMs} — one timer instead of two.
154
+ */
155
+ startTick() {
156
+ if (this.tickTimer !== undefined)
157
+ return;
158
+ const cadence = Math.min(this.pollIntervalMs, STALL_CHECK_MS);
159
+ this.tickTimer = setInterval(() => { this.tick(); }, cadence);
157
160
  }
158
- stopStallChecker() {
159
- if (this.stallTimer !== undefined) {
160
- clearInterval(this.stallTimer);
161
- this.stallTimer = undefined;
161
+ stopTick() {
162
+ if (this.tickTimer !== undefined) {
163
+ clearInterval(this.tickTimer);
164
+ this.tickTimer = undefined;
162
165
  }
163
166
  }
167
+ tick() {
168
+ if (this.stopped)
169
+ return;
170
+ if (this.observeSessionId === undefined)
171
+ return;
172
+ if (this.polling) {
173
+ // The stall phase has ended; the same tick now paces the polls.
174
+ if (this.now() >= this.nextPollAt) {
175
+ this.nextPollAt = this.now() + this.pollIntervalMs;
176
+ void this.pollTick();
177
+ }
178
+ return;
179
+ }
180
+ // A live SSE channel only goes silent while the agent idles; never poll
181
+ // against it. Fallback arms again only via onerror or a stream that has
182
+ // never delivered.
183
+ if (this.sseAlive)
184
+ return;
185
+ if ((this.now() - this.lastDataAt) > this.stallThresholdMs)
186
+ this.startPolling();
187
+ }
164
188
  startPolling() {
165
189
  if (this.polling || this.stopped)
166
190
  return;
167
191
  this.polling = true;
192
+ this.nextPollAt = this.now() + this.pollIntervalMs;
168
193
  void this.pollTick();
169
- this.pollTimer = setInterval(() => { void this.pollTick(); }, this.pollIntervalMs);
170
194
  }
171
195
  stopPolling() {
172
196
  this.polling = false;
173
- if (this.pollTimer !== undefined) {
174
- clearInterval(this.pollTimer);
175
- this.pollTimer = undefined;
176
- }
177
197
  }
178
198
  /**
179
199
  * Fetch the latest history page for the observed session and re-emit any
@@ -63,8 +63,9 @@ describe('MuxClient polling fallback', () => {
63
63
  // Nothing live within a poll interval until the stall window passes.
64
64
  await vi.advanceTimersByTimeAsync(400);
65
65
  expect(pollLatest).not.toHaveBeenCalled();
66
- // Past the stall threshold the first poll runs and emits seq 0.
67
- await vi.advanceTimersByTimeAsync(700); // 1100ms total -> first stall-checker tick
66
+ // The single tick (400 ms) crosses the 800 ms stall threshold at 1200 ms:
67
+ // the same tick then runs the first poll and emits seq 0.
68
+ await vi.advanceTimersByTimeAsync(800); // 1200ms total -> first stall crossing tick
68
69
  expect(pollLatest).toHaveBeenCalledWith('s1');
69
70
  expect(frames).toHaveLength(1);
70
71
  expect(frames[0]).toMatchObject({ type: 'session/event', sessionId: 's1' });
@@ -84,7 +85,7 @@ describe('MuxClient polling fallback', () => {
84
85
  client.onFrame(frame => { frames.push(frame); });
85
86
  client.start();
86
87
  client.observe('s1');
87
- await vi.advanceTimersByTimeAsync(1100); // first poll -> 3 events
88
+ await vi.advanceTimersByTimeAsync(1200); // first poll -> 3 events
88
89
  expect(frames).toHaveLength(3);
89
90
  await vi.advanceTimersByTimeAsync(400); // second poll -> same page, nothing new
90
91
  expect(frames).toHaveLength(3);
@@ -96,7 +97,7 @@ describe('MuxClient polling fallback', () => {
96
97
  const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
97
98
  client.start();
98
99
  client.observe('s1');
99
- await vi.advanceTimersByTimeAsync(1100);
100
+ await vi.advanceTimersByTimeAsync(1200);
100
101
  expect(pollLatest).toHaveBeenCalled();
101
102
  client.observe(undefined);
102
103
  const callsAfterClear = pollLatest.mock.calls.length;
@@ -110,7 +111,7 @@ describe('MuxClient polling fallback', () => {
110
111
  const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
111
112
  client.start();
112
113
  client.observe('s1');
113
- await vi.advanceTimersByTimeAsync(1100);
114
+ await vi.advanceTimersByTimeAsync(1200);
114
115
  expect(pollLatest).toHaveBeenCalled();
115
116
  client.stop();
116
117
  const callsAfterStop = pollLatest.mock.calls.length;
@@ -127,7 +128,7 @@ describe('MuxClient polling fallback', () => {
127
128
  client.start();
128
129
  client.observe('s1');
129
130
  // Stall into polling.
130
- await vi.advanceTimersByTimeAsync(1100);
131
+ await vi.advanceTimersByTimeAsync(1200);
131
132
  expect(pollLatest).toHaveBeenCalledTimes(1);
132
133
  // A live mux frame proves SSE delivers again -> fallback stops.
133
134
  sources[0]?.onmessage?.({ data: envelopeWith({ type: 'session/subscribed', sessionId: 's1', lastSeq: 4 }) });
@@ -138,4 +139,72 @@ describe('MuxClient polling fallback', () => {
138
139
  expect(live[0]).toMatchObject({ type: 'session/subscribed', sessionId: 's1' });
139
140
  client.stop();
140
141
  });
142
+ it('drives the whole lifecycle on a single scheduler tick (one interval)', async () => {
143
+ const { factory } = makeSources();
144
+ const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
145
+ try {
146
+ const pages = [pageOf([0]), pageOf([0, 1])];
147
+ const pollLatest = vi.fn(async (_sessionId) => pages.shift() ?? pageOf([]));
148
+ const client = new MuxClient('/m/api/events.mux', {
149
+ sourceFactory: factory,
150
+ pollLatest,
151
+ stallThresholdMs: 1500,
152
+ pollIntervalMs: 2000,
153
+ });
154
+ client.start();
155
+ client.observe('s1');
156
+ // Exactly one interval is ever created: the single tick scheduler.
157
+ expect(setIntervalSpy).toHaveBeenCalledTimes(1);
158
+ // The tick fires below the stall threshold without polling.
159
+ await vi.advanceTimersByTimeAsync(1000);
160
+ expect(pollLatest).not.toHaveBeenCalled();
161
+ // The same interval arms the fallback once silence passes the threshold
162
+ // (tick at 2000 crosses 1500) and runs the first poll immediately.
163
+ await vi.advanceTimersByTimeAsync(1100); // 2100ms total
164
+ expect(pollLatest).toHaveBeenCalledTimes(1);
165
+ expect(setIntervalSpy).toHaveBeenCalledTimes(1); // still one timer
166
+ // Subsequent polls ride the same tick at the poll cadence (2000 ms).
167
+ await vi.advanceTimersByTimeAsync(2000);
168
+ expect(pollLatest).toHaveBeenCalledTimes(2);
169
+ client.stop();
170
+ }
171
+ finally {
172
+ setIntervalSpy.mockRestore();
173
+ }
174
+ });
175
+ it('paces polls on the tick only after the stall phase ends', async () => {
176
+ const { factory } = makeSources();
177
+ const pages = [pageOf([0]), pageOf([0, 1]), pageOf([0, 1, 2]), pageOf([0, 1, 2, 3])];
178
+ const pollLatest = vi.fn(async (_sessionId) => pages.shift() ?? pageOf([]));
179
+ const client = new MuxClient('/m/api/events.mux', {
180
+ sourceFactory: factory,
181
+ pollLatest,
182
+ stallThresholdMs: 800,
183
+ pollIntervalMs: 400,
184
+ });
185
+ client.start();
186
+ client.observe('s1');
187
+ // At 1200 ms the stall threshold is crossed on this single 400 ms tick.
188
+ await vi.advanceTimersByTimeAsync(1200);
189
+ expect(pollLatest).toHaveBeenCalledTimes(1);
190
+ // Each subsequent 400 ms tick is a poll, matching the poll cadence.
191
+ await vi.advanceTimersByTimeAsync(400);
192
+ await vi.advanceTimersByTimeAsync(400);
193
+ await vi.advanceTimersByTimeAsync(400);
194
+ expect(pollLatest).toHaveBeenCalledTimes(4);
195
+ client.stop();
196
+ });
197
+ it('observe() already in the stall window starts the single-tick poller immediately', async () => {
198
+ const { factory } = makeSources();
199
+ const pollLatest = vi.fn(async (_sessionId) => pageOf([0]));
200
+ const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
201
+ client.start();
202
+ // Advance well past the stall threshold while nothing is observed.
203
+ await vi.advanceTimersByTimeAsync(2000);
204
+ expect(pollLatest).not.toHaveBeenCalled();
205
+ // Observing a session now is already in the stall window: poll right away.
206
+ client.observe('s1');
207
+ expect(pollLatest).toHaveBeenCalledTimes(1);
208
+ client.stop();
209
+ });
141
210
  });
@@ -29,6 +29,8 @@ export interface MobileApiDeps {
29
29
  apiProxy: ApiProxy;
30
30
  /** The resolved mobile composer preference (live per request). */
31
31
  mobileEnterToSend: () => boolean;
32
+ /** SSE keep-alive ping cadence for the mux stream (default 15000 ms; test seam). */
33
+ eventsHeartbeatMs?: number;
32
34
  }
33
35
  /** Mobile API route paths. */
34
36
  export declare const MOBILE_API_PATHS: {
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AA8ClD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;CACjC;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA2HnE"}
1
+ {"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAgDlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA4HnE"}