@linxin666/dsh-remote-web-ui 0.1.12 → 0.1.14

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 (68) hide show
  1. package/README.i18n.yaml +6 -0
  2. package/README.md +17 -3
  3. package/README.zh.md +171 -0
  4. package/lib/client.js +171 -100
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +338 -31
  7. package/lib/mobile.js +109 -33
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  10. package/lib/types/client/PluginSettingsCard.js +3 -2
  11. package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
  12. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  13. package/lib/types/client/RemoteSettingsCard.js +3 -1
  14. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  15. package/lib/types/client/UpdateEntry.js +10 -2
  16. package/lib/types/client/deep-link.js +10 -5
  17. package/lib/types/client/locales.d.ts +8 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/locales.js +10 -2
  20. package/lib/types/client/update-api.d.ts +10 -0
  21. package/lib/types/client/update-api.d.ts.map +1 -1
  22. package/lib/types/client/update-api.js +22 -2
  23. package/lib/types/index.d.ts +7 -0
  24. package/lib/types/index.d.ts.map +1 -1
  25. package/lib/types/index.js +7 -1
  26. package/lib/types/mobile/api.d.ts +9 -2
  27. package/lib/types/mobile/api.d.ts.map +1 -1
  28. package/lib/types/mobile/api.js +7 -3
  29. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  30. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  31. package/lib/types/mobile/mobile-styles.js +3 -0
  32. package/lib/types/mobile/views/App.d.ts +3 -1
  33. package/lib/types/mobile/views/App.d.ts.map +1 -1
  34. package/lib/types/mobile/views/App.js +18 -3
  35. package/lib/types/mobile/views/ChatView.d.ts +6 -0
  36. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  37. package/lib/types/mobile/views/ChatView.js +95 -7
  38. package/lib/types/mobile/views/ChatView.test.js +161 -3
  39. package/lib/types/mobile/views/WorkspaceView.d.ts +3 -1
  40. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  41. package/lib/types/mobile/views/WorkspaceView.js +9 -2
  42. package/lib/types/mobile/views/WorkspaceView.test.d.ts +2 -0
  43. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +1 -0
  44. package/lib/types/mobile/views/WorkspaceView.test.js +56 -0
  45. package/lib/types/mobile-api.d.ts +2 -0
  46. package/lib/types/mobile-api.d.ts.map +1 -1
  47. package/lib/types/mobile-api.js +17 -2
  48. package/lib/types/update.d.ts +5 -1
  49. package/lib/types/update.d.ts.map +1 -1
  50. package/lib/types/update.js +122 -26
  51. package/package.json +1 -11
  52. package/src/client/PluginSettingsCard.tsx +30 -5
  53. package/src/client/RemoteSettingsCard.tsx +18 -0
  54. package/src/client/UpdateEntry.tsx +10 -2
  55. package/src/client/deep-link.ts +10 -4
  56. package/src/client/locales.ts +10 -2
  57. package/src/client/settings-card.module.css +139 -144
  58. package/src/client/update-api.ts +22 -2
  59. package/src/index.ts +14 -1
  60. package/src/mobile/api.ts +14 -2
  61. package/src/mobile/mobile-styles.ts +3 -0
  62. package/src/mobile/views/App.tsx +22 -3
  63. package/src/mobile/views/ChatView.test.tsx +185 -3
  64. package/src/mobile/views/ChatView.tsx +102 -8
  65. package/src/mobile/views/WorkspaceView.test.tsx +66 -0
  66. package/src/mobile/views/WorkspaceView.tsx +11 -2
  67. package/src/mobile-api.ts +20 -2
  68. package/src/update.ts +116 -26
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { setInterval as setInterval$1 } from "node:timers";
3
- import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
+ import { Service } from "@deepseek-ai/cordis";
4
4
  import z from "schemastery";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { readFile } from "node:fs/promises";
@@ -11,6 +11,203 @@ import { networkInterfaces } from "node:os";
11
11
  import { Tunnel, bin, install } from "cloudflared";
12
12
  import { dirname, join } from "node:path";
13
13
  import { spawn } from "node:child_process";
14
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-settings@0.1.0-rc.6_@deepseek-ai+cordis@4.0.1_@deepseek-ai+dsh-brand@0_52495af71738a1095726249ceff4fd7b/node_modules/@deepseek-ai/dsh-settings/lib/index.js
15
+ /**
16
+ * Structural secret redaction for settings values. `role('secret')` fields are
17
+ * removed from a value before it crosses a wire boundary; a sidecar records
18
+ * each schema-declared secret position and whether it currently holds a value,
19
+ * so a configuration surface can render a write-only input without ever
20
+ * receiving the secret itself.
21
+ * @module @deepseek-ai/dsh-settings/redact
22
+ */
23
+ /** Whether a value is a plain data object the walker may recurse into. */
24
+ function isRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+ function walk(node, value, path, secrets) {
28
+ if (node === void 0) return value;
29
+ if (node.meta?.role === "secret") {
30
+ secrets.push({
31
+ path,
32
+ set: value !== void 0
33
+ });
34
+ return;
35
+ }
36
+ switch (node.type) {
37
+ case "object": {
38
+ const properties = node.dict ?? {};
39
+ const source = isRecord(value) ? value : void 0;
40
+ const rebuilt = {};
41
+ if (source !== void 0) for (const [key, entry] of Object.entries(source)) {
42
+ if (key in properties) continue;
43
+ rebuilt[key] = entry;
44
+ }
45
+ for (const [key, child] of Object.entries(properties)) {
46
+ const stripped = walk(child, source?.[key], [...path, key], secrets);
47
+ if (stripped !== void 0) rebuilt[key] = stripped;
48
+ }
49
+ return source === void 0 && Object.keys(rebuilt).length === 0 ? value : rebuilt;
50
+ }
51
+ case "dict": {
52
+ if (!isRecord(value)) return value;
53
+ const rebuilt = {};
54
+ for (const [key, entry] of Object.entries(value)) {
55
+ const stripped = walk(node.inner, entry, [...path, key], secrets);
56
+ if (stripped !== void 0) rebuilt[key] = stripped;
57
+ }
58
+ return rebuilt;
59
+ }
60
+ case "array":
61
+ if (!Array.isArray(value)) return value;
62
+ return value.map((entry, index) => walk(node.inner, entry, [...path, String(index)], secrets));
63
+ default: return value;
64
+ }
65
+ }
66
+ /**
67
+ * Service Definition for the user-settings capability seam (`ctx.settings`). Providers store one raw document of
68
+ * per-namespace sections; plugins register a namespace schema and read the
69
+ * resolved value, which layers schema defaults, the registrant's composition
70
+ * `base`, and the user document section, in that order.
71
+ * @module @deepseek-ai/dsh-settings
72
+ */
73
+ const NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*$/;
74
+ /**
75
+ * Brand a raw string as a {@link SettingsNamespace}.
76
+ * @param value - candidate namespace; lowercase kebab-case, as in plugin short names.
77
+ * @returns the branded namespace.
78
+ */
79
+ function settingsNamespace(value) {
80
+ if (!NAMESPACE_PATTERN.test(value)) throw new TypeError(`settings namespace "${value}" must match ${String(NAMESPACE_PATTERN)}`);
81
+ return value;
82
+ }
83
+ /**
84
+ * Deep equality over JSON-compatible data (objects, arrays, primitives) — the
85
+ * Service Definition's single change-detection predicate, exported so the invariant
86
+ * companion checks exactly the implementation's relation.
87
+ * @param a - one JSON-compatible value.
88
+ * @param b - the other JSON-compatible value.
89
+ * @returns whether the two values are structurally equal.
90
+ */
91
+ function deepEqualJson(a, b) {
92
+ if (a === b) return true;
93
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
94
+ if (Array.isArray(a) || Array.isArray(b)) {
95
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
96
+ return a.every((entry, index) => deepEqualJson(entry, b[index]));
97
+ }
98
+ const left = a;
99
+ const right = b;
100
+ const keys = Object.keys(left);
101
+ if (keys.length !== Object.keys(right).length) return false;
102
+ return keys.every((key) => key in right && deepEqualJson(left[key], right[key]));
103
+ }
104
+ /** Whether a value is a plain data object (not an array, null, or class instance). */
105
+ function isPlainObject(value) {
106
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
107
+ const proto = Object.getPrototypeOf(value);
108
+ return proto === Object.prototype || proto === null;
109
+ }
110
+ /** Apply one path op to a detached section, returning the next section. */
111
+ function applyPathOp(section, op) {
112
+ const [head, ...rest] = op.path;
113
+ if (head === void 0) {
114
+ if (op.op === "unset") return {};
115
+ if (!isPlainObject(op.value)) throw new TypeError("settings mutate: setting the section root requires a plain object");
116
+ return { ...op.value };
117
+ }
118
+ if (rest.length === 0) {
119
+ if (op.op === "set") return {
120
+ ...section,
121
+ [head]: op.value
122
+ };
123
+ const { [head]: _removed, ...kept } = section;
124
+ return kept;
125
+ }
126
+ const child = section[head];
127
+ if (!isPlainObject(child)) {
128
+ if (op.op === "unset") return section;
129
+ return {
130
+ ...section,
131
+ [head]: applyPathOp({}, {
132
+ ...op,
133
+ path: rest
134
+ })
135
+ };
136
+ }
137
+ return {
138
+ ...section,
139
+ [head]: applyPathOp(child, {
140
+ ...op,
141
+ path: rest
142
+ })
143
+ };
144
+ }
145
+ /**
146
+ * Layer `over` onto `under`: plain objects merge recursively, every other
147
+ * value (arrays included) replaces the lower layer wholesale. `over` never
148
+ * carries `undefined` entries — sections come from parsed documents and write
149
+ * snapshots pass {@link cloneJsonShaped}, which strips them so a sparse patch
150
+ * cannot erase lower keys.
151
+ */
152
+ function mergeLayers(under, over) {
153
+ if (over === void 0) return under;
154
+ if (!isPlainObject(under) || !isPlainObject(over)) return over;
155
+ const merged = { ...under };
156
+ for (const [key, value] of Object.entries(over)) merged[key] = key in merged ? mergeLayers(merged[key], value) : value;
157
+ return merged;
158
+ }
159
+ /** Recursively freeze one resolved value so handed-out snapshots stay immutable. */
160
+ function deepFreeze(value) {
161
+ if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
162
+ for (const entry of Object.values(value)) deepFreeze(entry);
163
+ return Object.freeze(value);
164
+ }
165
+ Service.init;
166
+ /**
167
+ * Value mirror of the `FiberState` members {@link isUnloading} compares
168
+ * against: a const enum has no runtime object to import, and the value is
169
+ * needed at runtime (same rationale as the CLI boot driver's mirror).
170
+ */
171
+ const FIBER_DISPOSED = 4;
172
+ const FIBER_UNLOADING = 5;
173
+ /** Whether the consumer's own fiber is tearing down (not just losing the settings service). */
174
+ function isUnloading(ctx) {
175
+ const state = ctx.fiber.state;
176
+ return state === FIBER_UNLOADING || state === FIBER_DISPOSED;
177
+ }
178
+ /**
179
+ * Install the canonical optional-settings consumer wiring: while a settings
180
+ * service exists, register `ns` with the consumer's composition entry as the
181
+ * `base` layer and point the source thunk at the resolved scope; when the
182
+ * service goes away (disposal, provider reload), fall back to the entry so
183
+ * the consumer keeps working exactly as composed. The registration rides the
184
+ * scoped fiber, so no settings service ever mounted means none of this runs.
185
+ * @param ctx - consumer plugin context owning the wiring.
186
+ * @param ns - the consumer-owned settings namespace.
187
+ * @param schema - schema resolving the namespace (typically the plugin Config).
188
+ * @param entry - the consumer's composition entry config, used as `base`.
189
+ * @param hooks - source sink and change notification.
190
+ */
191
+ function installSettingsSection(ctx, ns, schema, entry, hooks) {
192
+ ctx.inject(["settings"], (sctx) => {
193
+ const scope = sctx.settings.register(ns, schema, {
194
+ base: entry,
195
+ ...hooks.validate === void 0 ? {} : { validate: hooks.validate }
196
+ });
197
+ hooks.setSource(() => scope.get());
198
+ sctx.effect(() => () => {
199
+ if (isUnloading(ctx)) return;
200
+ hooks.setSource(() => entry);
201
+ hooks.onChange();
202
+ });
203
+ hooks.onChange();
204
+ scope.watch(() => {
205
+ if (isUnloading(ctx)) return;
206
+ hooks.onChange();
207
+ });
208
+ });
209
+ }
210
+ //#endregion
14
211
  //#region src/pairing.ts
15
212
  /**
16
213
  * Pairing state machine: one active one-time token, a device-session table,
@@ -803,6 +1000,12 @@ const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
803
1000
  "session.selectModel",
804
1001
  "session.rename"
805
1002
  ]);
1003
+ /**
1004
+ * Locally answered display-preference method (the phone's read-only
1005
+ * surface preferences; never proxied to the host ApiProxy and never a
1006
+ * settings-domain write).
1007
+ */
1008
+ const MOBILE_PREFERENCES_METHOD = "mobile.preferences";
806
1009
  /** One session.list page (thin phones load incrementally). */
807
1010
  const SESSION_PAGE_SIZE = 20;
808
1011
  /** Encode one list position as an opaque continuation cursor. */
@@ -836,7 +1039,7 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`;
836
1039
  * @returns the routes to register on webServer.
837
1040
  */
838
1041
  function makeMobileApiRoutes(deps) {
839
- const { service, apiProxy } = deps;
1042
+ const { service, apiProxy, mobileEnterToSend } = deps;
840
1043
  /** The phone gate: a live paired-device cookie, or nothing else proceeds. */
841
1044
  const gateOk = (req) => {
842
1045
  const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
@@ -874,7 +1077,8 @@ function makeMobileApiRoutes(deps) {
874
1077
  return;
875
1078
  }
876
1079
  const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length);
877
- if (!MOBILE_ALLOWLIST.has(method)) {
1080
+ const local = method === MOBILE_PREFERENCES_METHOD;
1081
+ if (!MOBILE_ALLOWLIST.has(method) && !local) {
878
1082
  writeJson(res, 403, {
879
1083
  ok: false,
880
1084
  error: {
@@ -909,6 +1113,17 @@ function makeMobileApiRoutes(deps) {
909
1113
  });
910
1114
  return;
911
1115
  }
1116
+ if (local) {
1117
+ writeJson(res, 200, {
1118
+ type: "server-response",
1119
+ rpcId,
1120
+ result: {
1121
+ ok: true,
1122
+ value: { mobileEnterToSend: mobileEnterToSend() }
1123
+ }
1124
+ });
1125
+ return;
1126
+ }
912
1127
  try {
913
1128
  const response = await dispatch(apiProxy, method, parsed?.payload, rpcId);
914
1129
  writeJson(res, 200, response);
@@ -1500,30 +1715,85 @@ async function checkUpdates(deps) {
1500
1715
  /** Cap on captured pnpm output (keeps error payloads bounded). */
1501
1716
  const OUTPUT_CAP = 16 * 1024;
1502
1717
  /**
1503
- * Run the update: `pnpm update <packages>` inside the profile directory.
1718
+ * Windows cmd command-not-found stderr. With shell:true a missing shim
1719
+ * exits with code 1 (cmd cannot report ENOENT), so the fallback chain
1720
+ * detects this message instead of the spawn error event.
1721
+ */
1722
+ const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i;
1723
+ /**
1724
+ * Run the update inside the profile directory. Tries pnpm first, falls back
1725
+ * to corepack and then npx when the previous command is missing (ENOENT);
1726
+ * all candidates share one hard timeout and keep accumulating output.
1504
1727
  * @param deps - profile dir, package list, and spawn/timeout seams.
1505
1728
  * @returns the outcome with captured output.
1506
1729
  */
1507
1730
  function runUpdate(deps) {
1508
1731
  return new Promise((resolve) => {
1509
- const child = (deps.spawnImpl ?? spawn)("pnpm", ["update", ...deps.packages], {
1732
+ const spawnImpl = deps.spawnImpl ?? spawn;
1733
+ const packages = deps.packages;
1734
+ const platform = deps.platform ?? process.platform;
1735
+ const spawnOptions = {
1510
1736
  cwd: deps.profileDir,
1511
1737
  stdio: [
1512
1738
  "ignore",
1513
1739
  "pipe",
1514
1740
  "pipe"
1515
- ]
1516
- });
1741
+ ],
1742
+ ...platform === "win32" ? { shell: true } : {}
1743
+ };
1744
+ const candidates = [
1745
+ {
1746
+ command: "pnpm",
1747
+ args: ["update", ...packages]
1748
+ },
1749
+ {
1750
+ command: "corepack",
1751
+ args: [
1752
+ "pnpm",
1753
+ "update",
1754
+ ...packages
1755
+ ]
1756
+ },
1757
+ {
1758
+ command: "npx",
1759
+ args: [
1760
+ "--yes",
1761
+ "pnpm",
1762
+ "update",
1763
+ ...packages
1764
+ ]
1765
+ }
1766
+ ];
1517
1767
  let output = "";
1768
+ let currentOutput = "";
1518
1769
  const append = (chunk) => {
1519
1770
  output += chunk.toString("utf8");
1520
1771
  if (output.length > OUTPUT_CAP) output = output.slice(output.length - OUTPUT_CAP);
1772
+ currentOutput += chunk.toString("utf8");
1773
+ if (currentOutput.length > OUTPUT_CAP) currentOutput = currentOutput.slice(currentOutput.length - OUTPUT_CAP);
1774
+ };
1775
+ let currentChild;
1776
+ let finished = false;
1777
+ const finish = (result) => {
1778
+ if (finished) return;
1779
+ finished = true;
1780
+ clearTimeout(timer);
1781
+ resolve(result);
1521
1782
  };
1522
- child.stdout?.on("data", append);
1523
- child.stderr?.on("data", append);
1524
1783
  const timer = setTimeout(() => {
1525
- child.kill("SIGTERM");
1526
- resolve({
1784
+ if (finished) return;
1785
+ if (platform === "win32") {
1786
+ const pid = currentChild?.pid;
1787
+ if (pid !== void 0 && pid > 0) try {
1788
+ spawnImpl("taskkill", [
1789
+ "/pid",
1790
+ String(pid),
1791
+ "/t",
1792
+ "/f"
1793
+ ], { stdio: "ignore" });
1794
+ } catch {}
1795
+ } else currentChild?.kill("SIGTERM");
1796
+ finish({
1527
1797
  ok: false,
1528
1798
  exitCode: null,
1529
1799
  output,
@@ -1531,27 +1801,59 @@ function runUpdate(deps) {
1531
1801
  errorCode: "timeout"
1532
1802
  });
1533
1803
  }, deps.timeoutMs ?? 10 * 6e4);
1534
- child.on("error", (error) => {
1535
- clearTimeout(timer);
1536
- const missing = error.code === "ENOENT";
1537
- resolve({
1538
- ok: false,
1539
- exitCode: null,
1540
- output,
1541
- error: missing ? "pnpm not found on PATH" : error.message,
1542
- errorCode: missing ? "pnpm-missing" : void 0
1804
+ const runCandidate = (index) => {
1805
+ if (finished) return;
1806
+ if (index >= candidates.length) {
1807
+ finish({
1808
+ ok: false,
1809
+ exitCode: null,
1810
+ output,
1811
+ error: "pnpm not found on PATH (tried pnpm, corepack, npx); install pnpm and restart the app",
1812
+ errorCode: "pnpm-missing"
1813
+ });
1814
+ return;
1815
+ }
1816
+ const candidate = candidates[index];
1817
+ currentOutput = "";
1818
+ const child = spawnImpl(candidate.command, candidate.args, spawnOptions);
1819
+ currentChild = child;
1820
+ let settled = false;
1821
+ const once = (fn) => {
1822
+ if (settled) return;
1823
+ settled = true;
1824
+ fn();
1825
+ };
1826
+ child.stdout?.on("data", append);
1827
+ child.stderr?.on("data", append);
1828
+ child.on("error", (error) => {
1829
+ once(() => {
1830
+ if (error.code === "ENOENT") runCandidate(index + 1);
1831
+ else finish({
1832
+ ok: false,
1833
+ exitCode: null,
1834
+ output,
1835
+ error: error.message,
1836
+ errorCode: void 0
1837
+ });
1838
+ });
1543
1839
  });
1544
- });
1545
- child.on("close", (code) => {
1546
- clearTimeout(timer);
1547
- resolve({
1548
- ok: code === 0,
1549
- exitCode: code,
1550
- output,
1551
- error: code === 0 ? void 0 : "pnpm exited with code " + String(code),
1552
- ...code === 0 ? {} : { errorCode: "pnpm-failed" }
1840
+ child.on("close", (code) => {
1841
+ if (settled || finished) return;
1842
+ if (platform === "win32" && code !== 0 && WIN_CMD_MISSING_RE.test(currentOutput)) {
1843
+ runCandidate(index + 1);
1844
+ return;
1845
+ }
1846
+ settled = true;
1847
+ finish({
1848
+ ok: code === 0,
1849
+ exitCode: code,
1850
+ output,
1851
+ error: code === 0 ? void 0 : "pnpm exited with code " + String(code),
1852
+ ...code === 0 ? {} : { errorCode: "pnpm-failed" }
1853
+ });
1553
1854
  });
1554
- });
1855
+ };
1856
+ runCandidate(0);
1555
1857
  });
1556
1858
  }
1557
1859
  //#endregion
@@ -1645,6 +1947,7 @@ const Config = z.object({
1645
1947
  requirePairingForLan: z.boolean().default(true),
1646
1948
  publicBaseUrl: z.string(),
1647
1949
  autoTunnel: z.boolean().default(false),
1950
+ mobileEnterToSend: z.boolean().default(true),
1648
1951
  enabled: z.boolean().default(true)
1649
1952
  });
1650
1953
  /** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
@@ -1658,6 +1961,7 @@ const DEFAULTS = {
1658
1961
  requirePairingForLan: true,
1659
1962
  publicBaseUrl: void 0,
1660
1963
  autoTunnel: false,
1964
+ mobileEnterToSend: true,
1661
1965
  enabled: true
1662
1966
  };
1663
1967
  /**
@@ -1674,6 +1978,7 @@ function apply(ctx, config) {
1674
1978
  requirePairingForLan: config?.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
1675
1979
  publicBaseUrl: config?.publicBaseUrl,
1676
1980
  autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
1981
+ mobileEnterToSend: config?.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
1677
1982
  enabled: config?.enabled ?? DEFAULTS.enabled
1678
1983
  };
1679
1984
  let current = () => config ?? {};
@@ -1687,6 +1992,7 @@ function apply(ctx, config) {
1687
1992
  requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
1688
1993
  publicBaseUrl: value.publicBaseUrl,
1689
1994
  autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
1995
+ mobileEnterToSend: value.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
1690
1996
  enabled: value.enabled ?? DEFAULTS.enabled
1691
1997
  };
1692
1998
  };
@@ -1771,7 +2077,8 @@ function apply(ctx, config) {
1771
2077
  ...makeMobileRoutes(),
1772
2078
  ...apiProxy !== void 0 ? makeMobileApiRoutes({
1773
2079
  service,
1774
- apiProxy
2080
+ apiProxy,
2081
+ mobileEnterToSend: () => resolve().mobileEnterToSend
1775
2082
  }) : [],
1776
2083
  ...updateRoutes
1777
2084
  ];