@linxin666/dsh-remote-web-ui 0.1.20 → 0.2.1

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 (126) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +56 -26
  3. package/README.zh.md +20 -15
  4. package/assets/mobile-icon-192.png +0 -0
  5. package/assets/mobile-icon-512.png +0 -0
  6. package/assets/mobile-service-worker.js +64 -0
  7. package/lib/client.js +272 -47
  8. package/lib/client.js.map +1 -1
  9. package/lib/index.js +316 -60
  10. package/lib/mobile.js +487 -56
  11. package/lib/mobile.js.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.d.ts +28 -0
  13. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PluginSettingsCard.js +147 -6
  15. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  16. package/lib/types/client/RemoteEntry.js +13 -2
  17. package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +1 -1
  20. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  21. package/lib/types/client/UpdateEntry.js +36 -3
  22. package/lib/types/client/UpdatePanel.d.ts.map +1 -1
  23. package/lib/types/client/UpdatePanel.js +2 -2
  24. package/lib/types/client/deep-link.js +1 -1
  25. package/lib/types/client/locales.d.ts +4 -0
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +4 -0
  28. package/lib/types/gate.d.ts +9 -0
  29. package/lib/types/gate.d.ts.map +1 -1
  30. package/lib/types/gate.js +17 -4
  31. package/lib/types/index.d.ts +25 -0
  32. package/lib/types/index.d.ts.map +1 -1
  33. package/lib/types/index.js +30 -13
  34. package/lib/types/mobile/PairRequiredView.d.ts +7 -0
  35. package/lib/types/mobile/PairRequiredView.d.ts.map +1 -0
  36. package/lib/types/mobile/PairRequiredView.js +28 -0
  37. package/lib/types/mobile/PairRequiredView.test.d.ts +2 -0
  38. package/lib/types/mobile/PairRequiredView.test.d.ts.map +1 -0
  39. package/lib/types/mobile/PairRequiredView.test.js +24 -0
  40. package/lib/types/mobile/api.d.ts +10 -0
  41. package/lib/types/mobile/api.d.ts.map +1 -1
  42. package/lib/types/mobile/api.js +4 -0
  43. package/lib/types/mobile/index.js +14 -1
  44. package/lib/types/mobile/messages.d.ts +24 -0
  45. package/lib/types/mobile/messages.d.ts.map +1 -1
  46. package/lib/types/mobile/messages.js +60 -23
  47. package/lib/types/mobile/messages.test.js +49 -1
  48. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  49. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  50. package/lib/types/mobile/mobile-styles.js +86 -0
  51. package/lib/types/mobile/pairing.d.ts +31 -0
  52. package/lib/types/mobile/pairing.d.ts.map +1 -0
  53. package/lib/types/mobile/pairing.js +62 -0
  54. package/lib/types/mobile/views/App.d.ts +12 -5
  55. package/lib/types/mobile/views/App.d.ts.map +1 -1
  56. package/lib/types/mobile/views/App.js +34 -7
  57. package/lib/types/mobile/views/App.test.d.ts +2 -0
  58. package/lib/types/mobile/views/App.test.d.ts.map +1 -0
  59. package/lib/types/mobile/views/App.test.js +52 -0
  60. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  61. package/lib/types/mobile/views/ChatView.js +34 -11
  62. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
  63. package/lib/types/mobile/views/SessionListView.js +30 -4
  64. package/lib/types/mobile/views/SessionListView.test.js +58 -1
  65. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  66. package/lib/types/mobile/views/WorkspaceView.js +4 -2
  67. package/lib/types/mobile-api.d.ts.map +1 -1
  68. package/lib/types/mobile-api.js +10 -1
  69. package/lib/types/mobile-pwa.d.ts +6 -0
  70. package/lib/types/mobile-pwa.d.ts.map +1 -0
  71. package/lib/types/mobile-pwa.js +17 -0
  72. package/lib/types/mobile-routes.d.ts +5 -8
  73. package/lib/types/mobile-routes.d.ts.map +1 -1
  74. package/lib/types/mobile-routes.js +115 -30
  75. package/lib/types/pairing-access.d.ts +28 -0
  76. package/lib/types/pairing-access.d.ts.map +1 -0
  77. package/lib/types/pairing-access.js +26 -0
  78. package/lib/types/pairing.d.ts +27 -0
  79. package/lib/types/pairing.d.ts.map +1 -1
  80. package/lib/types/pairing.js +68 -0
  81. package/lib/types/pairing.persist.test.d.ts +2 -0
  82. package/lib/types/pairing.persist.test.d.ts.map +1 -0
  83. package/lib/types/pairing.persist.test.js +128 -0
  84. package/lib/types/routes.d.ts.map +1 -1
  85. package/lib/types/routes.js +23 -3
  86. package/lib/types/tunnel.d.ts +1 -0
  87. package/lib/types/tunnel.d.ts.map +1 -1
  88. package/lib/types/tunnel.js +7 -2
  89. package/lib/types/update.d.ts +4 -4
  90. package/lib/types/update.d.ts.map +1 -1
  91. package/lib/types/update.js +35 -12
  92. package/package.json +12 -12
  93. package/src/client/PluginSettingsCard.tsx +229 -18
  94. package/src/client/RemoteEntry.tsx +12 -2
  95. package/src/client/RemoteSettingsCard.tsx +3 -2
  96. package/src/client/UpdateEntry.tsx +36 -4
  97. package/src/client/UpdatePanel.tsx +2 -0
  98. package/src/client/deep-link.ts +1 -1
  99. package/src/client/locales.ts +4 -0
  100. package/src/client/remote.module.css +26 -0
  101. package/src/client/settings-card.module.css +90 -1
  102. package/src/gate.ts +17 -3
  103. package/src/index.ts +46 -15
  104. package/src/mobile/PairRequiredView.test.tsx +28 -0
  105. package/src/mobile/PairRequiredView.tsx +58 -0
  106. package/src/mobile/api.ts +14 -1
  107. package/src/mobile/index.tsx +15 -1
  108. package/src/mobile/messages.test.ts +52 -1
  109. package/src/mobile/messages.ts +65 -22
  110. package/src/mobile/mobile-styles.ts +86 -0
  111. package/src/mobile/pairing.ts +77 -0
  112. package/src/mobile/views/App.test.tsx +62 -0
  113. package/src/mobile/views/App.tsx +53 -6
  114. package/src/mobile/views/ChatView.tsx +33 -10
  115. package/src/mobile/views/SessionListView.test.tsx +69 -1
  116. package/src/mobile/views/SessionListView.tsx +58 -5
  117. package/src/mobile/views/WorkspaceView.tsx +4 -2
  118. package/src/mobile-api.ts +9 -1
  119. package/src/mobile-pwa.ts +19 -0
  120. package/src/mobile-routes.ts +123 -30
  121. package/src/pairing-access.ts +36 -0
  122. package/src/pairing.persist.test.ts +145 -0
  123. package/src/pairing.ts +70 -1
  124. package/src/routes.ts +23 -3
  125. package/src/tunnel.ts +7 -2
  126. package/src/update.ts +42 -14
package/lib/mobile.js CHANGED
@@ -7578,6 +7578,10 @@ async function fetchMobilePreferences() {
7578
7578
  async function listSessions(cursor) {
7579
7579
  return await callUnary("session.list", cursor === void 0 ? {} : { cursor });
7580
7580
  }
7581
+ /** Read the available agent compositions for a new session. */
7582
+ async function listAgentPresets() {
7583
+ return await callUnary("agentPreset.list", {});
7584
+ }
7581
7585
  /**
7582
7586
  * Create a blank session (entity birth precedes the first message). Name a
7583
7587
  * workspace to attach it there, or a cwd; omitting both uses the host cwd.
@@ -11958,7 +11962,7 @@ function superRefine(fn, params) {
11958
11962
  return /* @__PURE__ */ _superRefine(fn, params);
11959
11963
  }
11960
11964
  //#endregion
11961
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/rpc.schema.js
11965
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.7_36988429fa7c61ceeac966dddad3597f/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/rpc.schema.js
11962
11966
  /**
11963
11967
  * Message-layer zod schemas: the four wire full forms + error body +
11964
11968
  * carrier receipt. The payload slot is unknown in the full-form schemas — business payloads
@@ -12139,11 +12143,6 @@ const rpcErrorSchema = discriminatedUnion("code", [
12139
12143
  message: string(),
12140
12144
  details: object({ ns: string() })
12141
12145
  }),
12142
- object({
12143
- code: literal("settings-not-exposed"),
12144
- message: string(),
12145
- details: object({ ns: string() })
12146
- }),
12147
12146
  object({
12148
12147
  code: literal("settings-conflict"),
12149
12148
  message: string(),
@@ -12272,7 +12271,7 @@ union([object({ accepted: literal(true) }), object({
12272
12271
  reason: union([literal("not-pending"), literal("bad-response")])
12273
12272
  })]);
12274
12273
  //#endregion
12275
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/session-search.js
12274
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.7_36988429fa7c61ceeac966dddad3597f/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/session-search.js
12276
12275
  /**
12277
12276
  * Return the longest prefix containing at most `maximum` Unicode code points.
12278
12277
  * @param value - text to bound.
@@ -12290,7 +12289,7 @@ function truncateUnicodeCodePoints(value, maximum) {
12290
12289
  return value;
12291
12290
  }
12292
12291
  //#endregion
12293
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/sessions.schema.js
12292
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.7_36988429fa7c61ceeac966dddad3597f/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/sessions.schema.js
12294
12293
  /**
12295
12294
  * sessions domain zod schemas (names derived from map keys: sessionListRequestSchema /
12296
12295
  * sessionListValueSchema). SessionEvent passthrough = strict envelope (type/seq/time) + wide
@@ -12526,7 +12525,7 @@ object({ accepted: literal(true) });
12526
12525
  object({ sessionId: sessionIdSchema });
12527
12526
  object({ accepted: literal(true) });
12528
12527
  //#endregion
12529
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/approvals.schema.js
12528
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.7_36988429fa7c61ceeac966dddad3597f/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/approvals.schema.js
12530
12529
  /**
12531
12530
  * approvals domain zod schemas (respond is a client-response; the payload schema serves
12532
12531
  * the /api/respond endpoint's second parse after routing via the pending table).
@@ -12560,7 +12559,7 @@ const taskViewSchema = object({
12560
12559
  finishedAt: number().int().nonnegative().optional()
12561
12560
  });
12562
12561
  //#endregion
12563
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/workspace.schema.js
12562
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.7_36988429fa7c61ceeac966dddad3597f/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/workspace.schema.js
12564
12563
  /**
12565
12564
  * workspace domain zod schemas (names derived from map keys). The
12566
12565
  * WorkspaceId brand cast lives in sessions.schema (see the note there) and
@@ -12606,7 +12605,7 @@ object({ workspace: workspaceViewSchema });
12606
12605
  object({ sessionId: sessionIdSchema });
12607
12606
  object({ archivedSessionIds: array(sessionIdSchema) });
12608
12607
  //#endregion
12609
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.6_8335eca05d5647f26dcc0c8fd5a39ec0/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/events.schema.js
12608
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-host-apiproxy@0.1.0-rc.7_36988429fa7c61ceeac966dddad3597f/node_modules/@deepseek-ai/dsh-host-apiproxy/lib/types/api/events.schema.js
12610
12609
  /**
12611
12610
  * events domain zod schemas: MuxFrame / HostFrame unions (discriminatedUnion('type')).
12612
12611
  * A frame is the payload slot of the ServerRequest full form; the SessionEvent inside
@@ -13057,13 +13056,6 @@ function chunkTarget(data) {
13057
13056
  if (step !== void 0) result.step = step;
13058
13057
  return result;
13059
13058
  }
13060
- /** Max message seq across an existing list, or -1 for an empty list. */
13061
- function watermarkOf(existing) {
13062
- if (existing === void 0) return -1;
13063
- let maxSeq = -1;
13064
- for (const message of existing) if (message.seq > maxSeq) maxSeq = message.seq;
13065
- return maxSeq;
13066
- }
13067
13059
  function createState(existing) {
13068
13060
  const messages = existing === void 0 ? [] : [...existing];
13069
13061
  const state = {
@@ -13072,9 +13064,11 @@ function createState(existing) {
13072
13064
  pendingByTurnStep: /* @__PURE__ */ new Map(),
13073
13065
  turnStepMessage: /* @__PURE__ */ new Map(),
13074
13066
  messageTurn: /* @__PURE__ */ new Map(),
13075
- toolNames: /* @__PURE__ */ new Map()
13067
+ toolNames: /* @__PURE__ */ new Map(),
13068
+ maxSeq: -1
13076
13069
  };
13077
13070
  for (const message of messages) {
13071
+ if (message.seq > state.maxSeq) state.maxSeq = message.seq;
13078
13072
  state.byId.set(message.id, message);
13079
13073
  if (message.kind !== "assistant") continue;
13080
13074
  const decoded = decodePendingTurnStep(message.id);
@@ -13128,6 +13122,7 @@ function retargetTurnStep(state, key, oldMessage, next) {
13128
13122
  }
13129
13123
  /** Fold one event into the working state. Assumes the event passes the watermark. */
13130
13124
  function applyEvent(state, event) {
13125
+ if (event.seq > state.maxSeq) state.maxSeq = event.seq;
13131
13126
  switch (event.type) {
13132
13127
  case "user/message":
13133
13128
  applyUserMessage(state, event);
@@ -13422,14 +13417,58 @@ function applyTurnEnd(state, event) {
13422
13417
  * @returns messages sorted by seq.
13423
13418
  */
13424
13419
  function foldEvents(events, existing) {
13425
- const sorted = [...events].sort((a, b) => a.seq - b.seq);
13426
- const watermark = watermarkOf(existing);
13427
- const state = createState(existing);
13428
- for (const event of sorted) {
13429
- if (event.seq <= watermark) continue;
13430
- applyEvent(state, event);
13431
- }
13432
- const out = state.messages;
13420
+ return new EventFolder(existing).fold(events);
13421
+ }
13422
+ /**
13423
+ * Incremental folder for one message stream. Live chat folds one event at a
13424
+ * time; rebuilding the five index maps by scanning every message per event
13425
+ * made that path O(n) per event (O(n * events) per turn). A folder keeps the
13426
+ * indexes alive across folds, applies each event in O(1) map operations, and
13427
+ * returns the previous snapshot identity unchanged when nothing applied, so
13428
+ * React skips the re-render entirely. Replayed events are no-ops: the maxSeq
13429
+ * watermark advanced by the first application skips them, which also makes a
13430
+ * double-invoked React state updater harmless.
13431
+ */
13432
+ var EventFolder = class {
13433
+ state;
13434
+ snapshotList;
13435
+ /** @param initial - seed rows (history tail load); omit for an empty stream. */
13436
+ constructor(initial) {
13437
+ this.state = createState(initial);
13438
+ }
13439
+ /** Fold one batch incrementally; returns the current snapshot list. */
13440
+ fold(events) {
13441
+ const sorted = [...events].sort((a, b) => a.seq - b.seq);
13442
+ let applied = false;
13443
+ for (const event of sorted) {
13444
+ if (event.seq <= this.state.maxSeq) continue;
13445
+ applyEvent(this.state, event);
13446
+ applied = true;
13447
+ }
13448
+ if (!applied && this.snapshotList !== void 0) return this.snapshotList;
13449
+ this.snapshotList = snapshotOf(this.state);
13450
+ return this.snapshotList;
13451
+ }
13452
+ /** Replace the whole stream (history reload / session switch). */
13453
+ seed(messages) {
13454
+ this.state = createState(messages);
13455
+ this.snapshotList = void 0;
13456
+ }
13457
+ /** Prepend an older history page (exact seam; no overlapping seqs). */
13458
+ prepend(older) {
13459
+ this.state = createState([...older, ...this.state.messages]);
13460
+ this.snapshotList = void 0;
13461
+ }
13462
+ /** Current snapshot list; a fresh copy whenever the folder changed. */
13463
+ snapshot() {
13464
+ if (this.snapshotList !== void 0) return this.snapshotList;
13465
+ this.snapshotList = snapshotOf(this.state);
13466
+ return this.snapshotList;
13467
+ }
13468
+ };
13469
+ /** Copy the folder's rows and keep them seq-ordered (skips re-sorting the common ordered case). */
13470
+ function snapshotOf(state) {
13471
+ const out = [...state.messages];
13433
13472
  let ordered = true;
13434
13473
  for (let index = 1; index < out.length; index += 1) {
13435
13474
  const prev = out[index - 1];
@@ -13439,7 +13478,7 @@ function foldEvents(events, existing) {
13439
13478
  break;
13440
13479
  }
13441
13480
  }
13442
- return ordered ? out : [...out].sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
13481
+ return ordered ? out : out.sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
13443
13482
  }
13444
13483
  //#endregion
13445
13484
  //#region src/mobile/markdown.ts
@@ -13916,6 +13955,8 @@ function ChatView({ session, mux, onBack }) {
13916
13955
  const tailLoadingRef = (0, import_react.useRef)(true);
13917
13956
  /** Live session events buffered while the initial tail page loads. */
13918
13957
  const liveBufferRef = (0, import_react.useRef)([]);
13958
+ /** Incremental folder for this session's stream (indexes stay hot across events). */
13959
+ const folderRef = (0, import_react.useRef)(void 0);
13919
13960
  /** True once the live buffer hit its cap (oldest events were dropped). */
13920
13961
  const liveBufferOverflowRef = (0, import_react.useRef)(false);
13921
13962
  /** The session's permission select (absent = capability not composed). */
@@ -13951,6 +13992,7 @@ function ChatView({ session, mux, onBack }) {
13951
13992
  tailLoadingRef.current = true;
13952
13993
  liveBufferRef.current = [];
13953
13994
  liveBufferOverflowRef.current = false;
13995
+ folderRef.current = void 0;
13954
13996
  setLoading(true);
13955
13997
  setError(void 0);
13956
13998
  setMessages([]);
@@ -13959,14 +14001,18 @@ function ChatView({ session, mux, onBack }) {
13959
14001
  const buffered = liveBufferRef.current;
13960
14002
  liveBufferRef.current = [];
13961
14003
  tailLoadingRef.current = false;
13962
- setMessages(foldEvents(buffered, foldEvents(page.events.map(eventOf))));
14004
+ const folder = new EventFolder(foldEvents(page.events.map(eventOf)));
14005
+ folderRef.current = folder;
14006
+ setMessages(folder.fold(buffered));
13963
14007
  setHasOlder(page.hasMore);
13964
14008
  setLoading(false);
13965
14009
  const projections = page.projections?.values;
13966
14010
  setPermissions(parsePermissionSelect(projections?.["permissions"]));
13967
14011
  if (liveBufferOverflowRef.current) loadHistory(session.sessionId, void 0, controller.signal).then((fresh) => {
13968
14012
  if (cancelled) return;
13969
- setMessages((previous) => foldEvents(fresh.events.map(eventOf), previous));
14013
+ const folder = folderRef.current;
14014
+ const freshEvents = fresh.events.map(eventOf);
14015
+ setMessages((previous) => folder === void 0 ? foldEvents(freshEvents, previous) : folder.fold(freshEvents));
13970
14016
  liveBufferOverflowRef.current = false;
13971
14017
  }, (reason) => {
13972
14018
  if (cancelled) return;
@@ -13977,7 +14023,10 @@ function ChatView({ session, mux, onBack }) {
13977
14023
  const buffered = liveBufferRef.current;
13978
14024
  liveBufferRef.current = [];
13979
14025
  tailLoadingRef.current = false;
13980
- if (buffered.length > 0) setMessages(foldEvents(buffered));
14026
+ if (buffered.length > 0) {
14027
+ const folder = folderRef.current;
14028
+ setMessages(folder === void 0 ? foldEvents(buffered) : folder.fold(buffered));
14029
+ }
13981
14030
  setError(errorText(reason));
13982
14031
  setLoading(false);
13983
14032
  });
@@ -14007,7 +14056,10 @@ function ChatView({ session, mux, onBack }) {
14007
14056
  liveBufferRef.current.push(event);
14008
14057
  return;
14009
14058
  }
14010
- setMessages((previous) => foldEvents([event], previous));
14059
+ setMessages((previous) => {
14060
+ const folder = folderRef.current;
14061
+ return folder === void 0 ? foldEvents([event], previous) : folder.fold([event]);
14062
+ });
14011
14063
  return;
14012
14064
  }
14013
14065
  if (frame.type === "session/projection" && frame.sessionId === session.sessionId && frame.key === "permissions") setPermissions(parsePermissionSelect(frame.value));
@@ -14044,7 +14096,12 @@ function ChatView({ session, mux, onBack }) {
14044
14096
  pendingRef.current = false;
14045
14097
  setLoading(false);
14046
14098
  const older = foldEvents(page.events.map(eventOf));
14047
- setMessages((previous) => [...older, ...previous]);
14099
+ const folder = folderRef.current;
14100
+ if (folder === void 0) setMessages((previous) => [...older, ...previous]);
14101
+ else {
14102
+ folder.prepend(older);
14103
+ setMessages(folder.snapshot());
14104
+ }
14048
14105
  setHasOlder(page.hasMore);
14049
14106
  }, (reason) => {
14050
14107
  pendingRef.current = false;
@@ -14279,8 +14336,12 @@ function ChatView({ session, mux, onBack }) {
14279
14336
  ]
14280
14337
  });
14281
14338
  }
14282
- /** One rendered message row (user bubble or assistant bubble with folds). */
14283
- function MessageRow({ message, showToolCalls, showSystemMessages }) {
14339
+ /**
14340
+ * One rendered message row (user bubble or assistant bubble with folds).
14341
+ * Memoized: live streaming updates exactly one message object per frame, so
14342
+ * unchanged rows skip re-rendering their markdown/sub-components.
14343
+ */
14344
+ const MessageRow = (0, import_react.memo)(function MessageRow({ message, showToolCalls, showSystemMessages }) {
14284
14345
  if (message.kind === "user" && message.sourceKind !== void 0 && message.sourceKind !== "user" && !showSystemMessages) return null;
14285
14346
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14286
14347
  className: `chat-msg chat-msg-${message.kind}${message.pending === true ? " chat-msg-pending" : ""}${message.failed === true ? " chat-msg-failed" : ""}`,
@@ -14301,7 +14362,7 @@ function MessageRow({ message, showToolCalls, showSystemMessages }) {
14301
14362
  })
14302
14363
  ]
14303
14364
  });
14304
- }
14365
+ });
14305
14366
  /** Collapsed-by-default reasoning disclosure (web-UI Think-row parity). */
14306
14367
  function ReasoningDisclosure({ text, pending }) {
14307
14368
  const [open, setOpen] = (0, import_react.useState)(false);
@@ -14853,6 +14914,9 @@ function SessionListView({ workspace, onBack, onPick }) {
14853
14914
  const [error, setError] = (0, import_react.useState)(void 0);
14854
14915
  const [creating, setCreating] = (0, import_react.useState)(false);
14855
14916
  const [createError, setCreateError] = (0, import_react.useState)(void 0);
14917
+ const [presets, setPresets] = (0, import_react.useState)([]);
14918
+ const [selectedPreset, setSelectedPreset] = (0, import_react.useState)(void 0);
14919
+ const [presetsLoading, setPresetsLoading] = (0, import_react.useState)(true);
14856
14920
  const cursorRef = (0, import_react.useRef)(void 0);
14857
14921
  const busyRef = (0, import_react.useRef)(false);
14858
14922
  (0, import_react.useEffect)(() => {
@@ -14875,6 +14939,25 @@ function SessionListView({ workspace, onBack, onPick }) {
14875
14939
  cancelled = true;
14876
14940
  };
14877
14941
  }, [workspace]);
14942
+ (0, import_react.useEffect)(() => {
14943
+ let cancelled = false;
14944
+ setPresets([]);
14945
+ setSelectedPreset(void 0);
14946
+ setPresetsLoading(true);
14947
+ listAgentPresets().then((roster) => {
14948
+ if (cancelled) return;
14949
+ const usable = roster.presets.filter((preset) => preset.broken === void 0);
14950
+ setPresets(roster.presets);
14951
+ setSelectedPreset((usable.find((preset) => preset.isDefault) ?? usable[0])?.id);
14952
+ setPresetsLoading(false);
14953
+ }, () => {
14954
+ if (cancelled) return;
14955
+ setPresetsLoading(false);
14956
+ });
14957
+ return () => {
14958
+ cancelled = true;
14959
+ };
14960
+ }, [workspace]);
14878
14961
  /** Pull the next page and append the rows that belong to this workspace. */
14879
14962
  const loadMore = (0, import_react.useCallback)(() => {
14880
14963
  if (busyRef.current) return;
@@ -14899,7 +14982,10 @@ function SessionListView({ workspace, onBack, onPick }) {
14899
14982
  if (creating) return;
14900
14983
  setCreating(true);
14901
14984
  setCreateError(void 0);
14902
- createSession({ workspaceId: workspace.workspaceId }).then((created) => {
14985
+ createSession({
14986
+ workspaceId: workspace.workspaceId,
14987
+ ...selectedPreset !== void 0 ? { agentPreset: selectedPreset } : {}
14988
+ }).then((created) => {
14903
14989
  setCreating(false);
14904
14990
  const view = {
14905
14991
  sessionId: created.sessionId,
@@ -14917,9 +15003,11 @@ function SessionListView({ workspace, onBack, onPick }) {
14917
15003
  }, [
14918
15004
  creating,
14919
15005
  workspace,
14920
- onPick
15006
+ onPick,
15007
+ selectedPreset
14921
15008
  ]);
14922
15009
  const createHint = createError !== void 0 ? staleHostHint(createError) : void 0;
15010
+ const selectedPresetEntry = presets.find((preset) => preset.id === selectedPreset);
14923
15011
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14924
15012
  className: "mobile",
14925
15013
  children: [
@@ -14944,17 +15032,50 @@ function SessionListView({ workspace, onBack, onPick }) {
14944
15032
  className: "mobile-error mobile-pad",
14945
15033
  children: error
14946
15034
  }),
14947
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
14948
- className: "mobile-pad",
14949
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
14950
- type: "button",
14951
- className: "mobile-new",
14952
- disabled: creating,
14953
- onClick: () => {
14954
- handleCreate();
14955
- },
14956
- children: creating ? "创建中…" : "+ 新建会话"
14957
- })
15035
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
15036
+ className: "mobile-create mobile-pad",
15037
+ children: [
15038
+ presets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
15039
+ className: "mobile-preset",
15040
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
15041
+ className: "mobile-presetLabel",
15042
+ children: "Agent 模式"
15043
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
15044
+ className: "mobile-presetSelect",
15045
+ value: selectedPreset ?? "",
15046
+ disabled: selectedPreset === void 0,
15047
+ onChange: (event) => {
15048
+ setSelectedPreset(event.target.value);
15049
+ },
15050
+ children: [selectedPreset === void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
15051
+ value: "",
15052
+ children: "无可用 Agent 模式"
15053
+ }), presets.map((preset) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("option", {
15054
+ value: preset.id,
15055
+ disabled: preset.broken !== void 0,
15056
+ children: [
15057
+ preset.name ?? preset.id,
15058
+ preset.isDefault ? "(默认)" : "",
15059
+ preset.trust === "user" ? "(本地)" : "",
15060
+ preset.broken !== void 0 ? "(不可用)" : ""
15061
+ ]
15062
+ }, preset.id))]
15063
+ })]
15064
+ }),
15065
+ selectedPresetEntry?.description !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
15066
+ className: "mobile-presetDescription",
15067
+ children: selectedPresetEntry.description
15068
+ }),
15069
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
15070
+ type: "button",
15071
+ className: "mobile-new",
15072
+ disabled: creating || presetsLoading,
15073
+ onClick: () => {
15074
+ handleCreate();
15075
+ },
15076
+ children: creating ? "创建中…" : "+ 新建会话"
15077
+ })
15078
+ ]
14958
15079
  }),
14959
15080
  createError !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
14960
15081
  className: "mobile-error mobile-pad",
@@ -15027,6 +15148,7 @@ function SessionListView({ workspace, onBack, onPick }) {
15027
15148
  function WorkspaceView({ initialWorkspaceId, onPick }) {
15028
15149
  const [items, setItems] = (0, import_react.useState)(void 0);
15029
15150
  const [error, setError] = (0, import_react.useState)(void 0);
15151
+ const [reload, setReload] = (0, import_react.useState)(0);
15030
15152
  (0, import_react.useEffect)(() => {
15031
15153
  let cancelled = false;
15032
15154
  listWorkspaces().then((rows) => {
@@ -15044,7 +15166,11 @@ function WorkspaceView({ initialWorkspaceId, onPick }) {
15044
15166
  return () => {
15045
15167
  cancelled = true;
15046
15168
  };
15047
- }, [initialWorkspaceId, onPick]);
15169
+ }, [
15170
+ initialWorkspaceId,
15171
+ onPick,
15172
+ reload
15173
+ ]);
15048
15174
  if (error !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
15049
15175
  className: "mobile",
15050
15176
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
@@ -15064,6 +15190,7 @@ function WorkspaceView({ initialWorkspaceId, onPick }) {
15064
15190
  onClick: () => {
15065
15191
  setError(void 0);
15066
15192
  setItems(void 0);
15193
+ setReload((n) => n + 1);
15067
15194
  },
15068
15195
  children: "重试"
15069
15196
  })]
@@ -15136,6 +15263,154 @@ function WorkspaceView({ initialWorkspaceId, onPick }) {
15136
15263
  });
15137
15264
  }
15138
15265
  //#endregion
15266
+ //#region src/mobile/pairing.ts
15267
+ const MOBILE_ROOT = "/m/";
15268
+ /** Parse a pairing token or a copied pairing link without following its origin. */
15269
+ function parseMobilePairInput(value) {
15270
+ const trimmed = value.trim();
15271
+ if (trimmed === "") return void 0;
15272
+ try {
15273
+ const url = new URL(trimmed);
15274
+ const token = url.searchParams.get("pair");
15275
+ if (token === null || token === "") return void 0;
15276
+ const workspaceId = url.searchParams.get("workspace");
15277
+ return {
15278
+ token,
15279
+ ...workspaceId !== null && workspaceId !== "" ? { workspaceId } : {}
15280
+ };
15281
+ } catch {
15282
+ return { token: trimmed };
15283
+ }
15284
+ }
15285
+ /** Build the safe, token-free mobile destination after successful pairing. */
15286
+ function mobilePairPath(workspaceId) {
15287
+ return workspaceId === void 0 ? MOBILE_ROOT : "/m/?workspace=" + encodeURIComponent(workspaceId);
15288
+ }
15289
+ /** Accept one pairing token on this exact browser or installed-web-app context. */
15290
+ async function acceptMobilePair(token, fetcher = fetch) {
15291
+ try {
15292
+ const response = await fetcher("/api/pair/accept", {
15293
+ method: "POST",
15294
+ headers: { "content-type": "application/json" },
15295
+ body: JSON.stringify({ token })
15296
+ });
15297
+ if (response.ok) return { ok: true };
15298
+ if (response.status === 404) return {
15299
+ ok: false,
15300
+ message: "配对链接无效或已过期。"
15301
+ };
15302
+ if (response.status === 409) return {
15303
+ ok: false,
15304
+ message: "配对链接已被使用。"
15305
+ };
15306
+ return {
15307
+ ok: false,
15308
+ message: "此设备无法使用该配对链接。"
15309
+ };
15310
+ } catch {
15311
+ return {
15312
+ ok: false,
15313
+ message: "无法连接到配对服务。"
15314
+ };
15315
+ }
15316
+ }
15317
+ /** Consume a QR pairing token before the mobile application starts making RPC calls. */
15318
+ async function consumeMobilePairUrl(href, fetcher = fetch) {
15319
+ let url;
15320
+ try {
15321
+ url = new URL(href);
15322
+ } catch {
15323
+ return { kind: "none" };
15324
+ }
15325
+ const token = url.searchParams.get("pair");
15326
+ if (token === null || token === "") return { kind: "none" };
15327
+ const workspaceId = url.searchParams.get("workspace");
15328
+ const path = mobilePairPath(workspaceId === null || workspaceId === "" ? void 0 : workspaceId);
15329
+ const result = await acceptMobilePair(token, fetcher);
15330
+ return result.ok ? {
15331
+ kind: "accepted",
15332
+ path
15333
+ } : {
15334
+ kind: "failed",
15335
+ path,
15336
+ message: result.message
15337
+ };
15338
+ }
15339
+ //#endregion
15340
+ //#region src/mobile/PairRequiredView.tsx
15341
+ /** Device-pairing gate for installed mobile web-app contexts with isolated storage. */
15342
+ /** Collect a fresh desktop-issued pairing link when this client has no paired cookie. */
15343
+ function PairRequiredView({ initialError, onPaired }) {
15344
+ const [value, setValue] = (0, import_react.useState)("");
15345
+ const [error, setError] = (0, import_react.useState)(initialError);
15346
+ const [submitting, setSubmitting] = (0, import_react.useState)(false);
15347
+ const submit = async (event) => {
15348
+ event.preventDefault();
15349
+ const input = parseMobilePairInput(value);
15350
+ if (input === void 0) {
15351
+ setError("请输入有效的配对链接。");
15352
+ return;
15353
+ }
15354
+ setSubmitting(true);
15355
+ setError(void 0);
15356
+ const result = await acceptMobilePair(input.token);
15357
+ setSubmitting(false);
15358
+ if (!result.ok) {
15359
+ setError(result.message);
15360
+ return;
15361
+ }
15362
+ onPaired(mobilePairPath(input.workspaceId));
15363
+ };
15364
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("main", {
15365
+ className: "mobile mobile-pair",
15366
+ "aria-labelledby": "mobile-pair-title",
15367
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", {
15368
+ className: "mobile-pairCard",
15369
+ onSubmit: (event) => {
15370
+ submit(event);
15371
+ },
15372
+ children: [
15373
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
15374
+ id: "mobile-pair-title",
15375
+ className: "mobile-title",
15376
+ children: "设备配对"
15377
+ }),
15378
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
15379
+ className: "mobile-muted",
15380
+ children: "粘贴桌面端复制的配对链接以连接此设备。"
15381
+ }),
15382
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
15383
+ className: "mobile-pairLabel",
15384
+ htmlFor: "mobile-pair-link",
15385
+ children: "配对链接"
15386
+ }),
15387
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
15388
+ id: "mobile-pair-link",
15389
+ className: "mobile-pairInput",
15390
+ value,
15391
+ onChange: (event) => {
15392
+ setValue(event.target.value);
15393
+ },
15394
+ autoComplete: "off",
15395
+ autoCapitalize: "off",
15396
+ spellCheck: false
15397
+ }),
15398
+ error === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
15399
+ className: "mobile-error",
15400
+ role: "alert",
15401
+ children: error
15402
+ }),
15403
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
15404
+ className: "mobile-new mobile-pairSubmit",
15405
+ type: "submit",
15406
+ disabled: submitting,
15407
+ children: submitting ? "正在配对" : "配对"
15408
+ })
15409
+ ]
15410
+ })
15411
+ });
15412
+ }
15413
+ //#endregion
15139
15414
  //#region src/mobile/views/App.tsx
15140
15415
  /**
15141
15416
  * Mobile surface root: the view state machine (workspaces → sessions →
@@ -15172,11 +15447,60 @@ function formatTime(epochMs) {
15172
15447
  if (date.toDateString() === yesterday.toDateString()) return `昨天 ${clock}`;
15173
15448
  return `${String(date.getMonth() + 1)}月${String(date.getDate())}日 ${clock}`;
15174
15449
  }
15175
- /**
15176
- * The surface root.
15177
- * @returns the app tree.
15178
- */
15179
- function App() {
15450
+ /** Whether the mobile gateway rejected this browser for lack of a paired cookie. */
15451
+ function isUnpairedMobileError(error) {
15452
+ return error instanceof RpcTransportError && error.message === "HTTP 403";
15453
+ }
15454
+ /** Classify an initial mobile preference failure without treating outage as authorization. */
15455
+ function mobilePairStateForError(error) {
15456
+ return isUnpairedMobileError(error) ? "unpaired" : "unavailable";
15457
+ }
15458
+ /** Gate the independent mobile bundle until its own browser context is paired. */
15459
+ function App({ initialPairError }) {
15460
+ const [pairState, setPairState] = (0, import_react.useState)("checking");
15461
+ (0, import_react.useEffect)(() => {
15462
+ let current = true;
15463
+ fetchMobilePreferences().then(() => {
15464
+ if (current) setPairState("paired");
15465
+ }, (error) => {
15466
+ if (current) setPairState(mobilePairStateForError(error));
15467
+ });
15468
+ return () => {
15469
+ current = false;
15470
+ };
15471
+ }, []);
15472
+ if (pairState === "checking") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("main", {
15473
+ className: "mobile mobile-empty",
15474
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
15475
+ className: "mobile-muted",
15476
+ children: "正在连接..."
15477
+ })
15478
+ });
15479
+ if (pairState === "unpaired") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PairRequiredView, {
15480
+ initialError: initialPairError,
15481
+ onPaired: (path) => {
15482
+ window.location.replace(path);
15483
+ }
15484
+ });
15485
+ if (pairState === "unavailable") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("main", {
15486
+ className: "mobile mobile-empty",
15487
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
15488
+ className: "mobile-error",
15489
+ role: "alert",
15490
+ children: "无法连接到运行中的 DSH host。"
15491
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
15492
+ className: "mobile-new",
15493
+ type: "button",
15494
+ onClick: () => {
15495
+ window.location.reload();
15496
+ },
15497
+ children: "重试"
15498
+ })]
15499
+ });
15500
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PairedApp, {});
15501
+ }
15502
+ /** The existing remote mobile surface, mounted only after device pairing succeeds. */
15503
+ function PairedApp() {
15180
15504
  const [route, setRoute] = (0, import_react.useState)({ kind: "workspaces" });
15181
15505
  const [initialWorkspaceId, setInitialWorkspaceId] = (0, import_react.useState)(() => mobileWorkspaceTarget(window.location.search));
15182
15506
  const muxRef = (0, import_react.useRef)(void 0);
@@ -15515,6 +15839,43 @@ body {
15515
15839
  padding: 8px 16px;
15516
15840
  }
15517
15841
 
15842
+ .mobile-create {
15843
+ display: grid;
15844
+ gap: 8px;
15845
+ }
15846
+
15847
+ .mobile-preset {
15848
+ display: grid;
15849
+ gap: 4px;
15850
+ }
15851
+
15852
+ .mobile-presetLabel {
15853
+ color: var(--m-text-secondary);
15854
+ font-size: 13px;
15855
+ font-weight: 600;
15856
+ }
15857
+
15858
+ .mobile-presetSelect {
15859
+ width: 100%;
15860
+ height: 40px;
15861
+ padding: 0 36px 0 12px;
15862
+ border: 1px solid var(--m-border);
15863
+ border-radius: 10px;
15864
+ background: var(--m-bg-input);
15865
+ color: var(--m-text);
15866
+ font: inherit;
15867
+ }
15868
+
15869
+ .mobile-presetSelect:disabled {
15870
+ opacity: 0.6;
15871
+ }
15872
+
15873
+ .mobile-presetDescription {
15874
+ margin: 0;
15875
+ color: var(--m-text-tertiary);
15876
+ font-size: 12px;
15877
+ }
15878
+
15518
15879
  /* ── buttons ─────────────────────────────────────────────────────────── */
15519
15880
 
15520
15881
  .mobile-button {
@@ -16159,6 +16520,7 @@ body {
16159
16520
  .mobile-row:focus-visible,
16160
16521
  .mobile-button:focus-visible,
16161
16522
  .mobile-new:focus-visible,
16523
+ .mobile-presetSelect:focus-visible,
16162
16524
  .chat-send:focus-visible,
16163
16525
  .chat-chip:focus-visible,
16164
16526
  .chat-msg-toggle:focus-visible,
@@ -16283,7 +16645,66 @@ body {
16283
16645
  transition: none;
16284
16646
  }
16285
16647
  }
16648
+
16649
+ /* ── installed-app pairing ───────────────────────────────────────────── */
16650
+
16651
+ .mobile-pair {
16652
+ min-height: 100dvh;
16653
+ display: grid;
16654
+ place-items: center;
16655
+ padding: 24px;
16656
+ }
16657
+
16658
+ .mobile-pairCard {
16659
+ display: grid;
16660
+ width: min(100%, 420px);
16661
+ gap: 12px;
16662
+ padding: 24px;
16663
+ border: 1px solid var(--m-border);
16664
+ border-radius: 12px;
16665
+ background: var(--m-bg-raised);
16666
+ box-shadow: var(--m-shadow-raise);
16667
+ }
16668
+
16669
+ .mobile-pairLabel {
16670
+ color: var(--m-text-secondary);
16671
+ font-size: 13px;
16672
+ font-weight: 600;
16673
+ }
16674
+
16675
+ .mobile-pairInput {
16676
+ width: 100%;
16677
+ min-height: 42px;
16678
+ box-sizing: border-box;
16679
+ padding: 9px 12px;
16680
+ border: 1px solid var(--m-border);
16681
+ border-radius: 8px;
16682
+ background: var(--m-bg-input);
16683
+ color: var(--m-text);
16684
+ font: inherit;
16685
+ outline: none;
16686
+ }
16687
+
16688
+ .mobile-pairInput:focus-visible {
16689
+ border-color: var(--m-accent);
16690
+ box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);
16691
+ }
16692
+
16693
+ .mobile-pairSubmit {
16694
+ width: 100%;
16695
+ }
16286
16696
  `;
16697
+ const MOBILE_SERVICE_WORKER_URL = "/m/service-worker.js";
16698
+ /** Register the mobile shell worker when the current browser supports it. */
16699
+ async function registerMobilePwa(serviceWorker = typeof navigator === "undefined" ? void 0 : navigator.serviceWorker) {
16700
+ if (serviceWorker === void 0) return;
16701
+ try {
16702
+ await serviceWorker.register(MOBILE_SERVICE_WORKER_URL, {
16703
+ scope: "/m/",
16704
+ updateViaCache: "none"
16705
+ });
16706
+ } catch {}
16707
+ }
16287
16708
  //#endregion
16288
16709
  //#region src/mobile/index.tsx
16289
16710
  /**
@@ -16301,7 +16722,17 @@ style.textContent = mobileCss;
16301
16722
  document.head.appendChild(style);
16302
16723
  const root = document.getElementById("root");
16303
16724
  if (root === null) throw new Error("mobile: #root missing");
16304
- (0, import_client.createRoot)(root).render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(App, {}));
16725
+ bootMobile(root);
16726
+ async function bootMobile(mount) {
16727
+ const pair = await consumeMobilePairUrl(window.location.href);
16728
+ if (pair.kind === "accepted") {
16729
+ window.location.replace(pair.path);
16730
+ return;
16731
+ }
16732
+ if (pair.kind === "failed") window.history.replaceState(null, "", pair.path);
16733
+ registerMobilePwa();
16734
+ (0, import_client.createRoot)(mount).render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(App, { initialPairError: pair.kind === "failed" ? pair.message : void 0 }));
16735
+ }
16305
16736
  //#endregion
16306
16737
 
16307
16738
  //# sourceMappingURL=mobile.js.map