@linxin666/dsh-remote-web-ui 0.2.0 → 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 (68) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +15 -7
  3. package/README.zh.md +4 -4
  4. package/lib/client.js +235 -37
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +26 -7
  7. package/lib/mobile.js +209 -39
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.d.ts +22 -0
  10. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  11. package/lib/types/client/PluginSettingsCard.js +142 -3
  12. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  13. package/lib/types/client/RemoteEntry.js +11 -0
  14. package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
  15. package/lib/types/client/UpdatePanel.d.ts.map +1 -1
  16. package/lib/types/client/UpdatePanel.js +2 -2
  17. package/lib/types/client/locales.d.ts +2 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/locales.js +2 -0
  20. package/lib/types/gate.d.ts.map +1 -1
  21. package/lib/types/gate.js +2 -0
  22. package/lib/types/mobile/api.d.ts +10 -0
  23. package/lib/types/mobile/api.d.ts.map +1 -1
  24. package/lib/types/mobile/api.js +4 -0
  25. package/lib/types/mobile/messages.d.ts +24 -0
  26. package/lib/types/mobile/messages.d.ts.map +1 -1
  27. package/lib/types/mobile/messages.js +60 -23
  28. package/lib/types/mobile/messages.test.js +49 -1
  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 +38 -0
  32. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  33. package/lib/types/mobile/views/ChatView.js +34 -11
  34. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
  35. package/lib/types/mobile/views/SessionListView.js +30 -4
  36. package/lib/types/mobile/views/SessionListView.test.js +58 -1
  37. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  38. package/lib/types/mobile/views/WorkspaceView.js +4 -2
  39. package/lib/types/mobile-api.d.ts.map +1 -1
  40. package/lib/types/mobile-api.js +10 -1
  41. package/lib/types/routes.d.ts.map +1 -1
  42. package/lib/types/routes.js +22 -2
  43. package/lib/types/tunnel.d.ts +1 -0
  44. package/lib/types/tunnel.d.ts.map +1 -1
  45. package/lib/types/tunnel.js +7 -2
  46. package/lib/types/update.d.ts +1 -1
  47. package/lib/types/update.d.ts.map +1 -1
  48. package/lib/types/update.js +1 -1
  49. package/package.json +1 -1
  50. package/src/client/PluginSettingsCard.tsx +219 -18
  51. package/src/client/RemoteEntry.tsx +10 -0
  52. package/src/client/RemoteSettingsCard.tsx +2 -2
  53. package/src/client/UpdatePanel.tsx +2 -0
  54. package/src/client/locales.ts +2 -0
  55. package/src/client/settings-card.module.css +90 -1
  56. package/src/gate.ts +2 -0
  57. package/src/mobile/api.ts +14 -1
  58. package/src/mobile/messages.test.ts +52 -1
  59. package/src/mobile/messages.ts +65 -22
  60. package/src/mobile/mobile-styles.ts +38 -0
  61. package/src/mobile/views/ChatView.tsx +33 -10
  62. package/src/mobile/views/SessionListView.test.tsx +69 -1
  63. package/src/mobile/views/SessionListView.tsx +58 -5
  64. package/src/mobile/views/WorkspaceView.tsx +4 -2
  65. package/src/mobile-api.ts +9 -1
  66. package/src/routes.ts +22 -2
  67. package/src/tunnel.ts +7 -2
  68. package/src/update.ts +2 -2
package/lib/index.js CHANGED
@@ -868,7 +868,9 @@ function makeRoutes(deps) {
868
868
  const ACCEPT_MAX_ATTEMPTS = 10;
869
869
  const ACCEPT_WINDOW_MS = 3e4;
870
870
  const rateLimitAccept = (req) => {
871
- const ip = req.socket?.remoteAddress ?? "unknown";
871
+ const socketIp = req.socket?.remoteAddress ?? "unknown";
872
+ const forwarded = typeof req.headers["x-forwarded-for"] === "string" ? (req.headers["x-forwarded-for"].split(",")[0] ?? "").trim() : void 0;
873
+ const ip = forwarded === void 0 || forwarded === "" ? socketIp : socketIp + "|" + forwarded;
872
874
  const nowMs = Date.now();
873
875
  if (acceptAttempts.size > 256) {
874
876
  for (const [key, attempt] of acceptAttempts) if (nowMs - attempt.windowStart > ACCEPT_WINDOW_MS) acceptAttempts.delete(key);
@@ -1022,10 +1024,16 @@ function makeRoutes(deps) {
1022
1024
  return;
1023
1025
  }
1024
1026
  const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
1027
+ const paired = deviceId !== void 0 && service.hasDevice(deviceId);
1028
+ const snapshot = service.snapshot();
1025
1029
  writeJson(res, 200, {
1026
1030
  ok: true,
1027
- paired: deviceId !== void 0 && service.hasDevice(deviceId),
1028
- ...service.snapshot()
1031
+ paired,
1032
+ ...paired ? snapshot : {
1033
+ phase: snapshot.phase,
1034
+ lanAvailable: snapshot.lanAvailable,
1035
+ lanAddresses: snapshot.lanAddresses
1036
+ }
1029
1037
  });
1030
1038
  };
1031
1039
  const handleEvents = (req, res) => {
@@ -1306,9 +1314,16 @@ function makeMobileRoutes() {
1306
1314
  }
1307
1315
  //#endregion
1308
1316
  //#region src/mobile-api.ts
1309
- /** Methods the phone surface may call. Everything else is refused. */
1317
+ /**
1318
+ * Methods the phone surface may call. Everything else is refused HERE — but
1319
+ * note the paired-device cookie also passes the global api/gate for the full
1320
+ * ApiProxy surface (gate.ts), so a paired phone is a full-control credential:
1321
+ * the allowlist only constrains this /m/api proxy, not the cookie's reach.
1322
+ * stop() revokes every device; there is no per-device revocation yet.
1323
+ */
1310
1324
  const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
1311
1325
  "workspace.list",
1326
+ "agentPreset.list",
1312
1327
  "session.create",
1313
1328
  "session.list",
1314
1329
  "session.history",
@@ -1573,6 +1588,7 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
1573
1588
  result: response.result
1574
1589
  });
1575
1590
  if (method === "workspace.list") return wrap(await apiProxy.workspace.list(request));
1591
+ if (method === "agentPreset.list") return wrap(await apiProxy.agentPresets.list(request));
1576
1592
  if (method === "session.create") return wrap(await apiProxy.sessions.create(request));
1577
1593
  if (method === "session.history") return wrap(await apiProxy.sessions.history(request));
1578
1594
  if (method === "session.search") return wrap(await apiProxy.sessions.search(request, signal ?? new AbortController().signal));
@@ -1650,6 +1666,7 @@ var TunnelManager = class {
1650
1666
  urlTimer;
1651
1667
  restartTimer;
1652
1668
  attempts = 0;
1669
+ generation = 0;
1653
1670
  stopping = false;
1654
1671
  urlListeners = /* @__PURE__ */ new Set();
1655
1672
  phaseListeners = /* @__PURE__ */ new Set();
@@ -1684,6 +1701,7 @@ var TunnelManager = class {
1684
1701
  this.stopping = false;
1685
1702
  this.targetUrl = targetUrl;
1686
1703
  this.attempts = 0;
1704
+ this.generation += 1;
1687
1705
  this.attempt();
1688
1706
  }
1689
1707
  /** Stop the tunnel for good: no restarts, no state. */
@@ -1713,12 +1731,13 @@ var TunnelManager = class {
1713
1731
  }
1714
1732
  attempt() {
1715
1733
  if (this.stopping || this.targetUrl === void 0) return;
1734
+ const gen = this.generation;
1716
1735
  this.setPhase("starting");
1717
1736
  this.handle = void 0;
1718
1737
  this.url = void 0;
1719
1738
  this.error = void 0;
1720
1739
  this.ensureBinary().then(() => {
1721
- if (this.stopping || this.targetUrl === void 0) return;
1740
+ if (this.stopping || this.targetUrl === void 0 || gen !== this.generation) return;
1722
1741
  const handle = this.factory(this.targetUrl);
1723
1742
  this.handle = handle;
1724
1743
  this.urlTimer = this.timer.setTimeout(() => {
@@ -1737,7 +1756,7 @@ var TunnelManager = class {
1737
1756
  this.error = value instanceof Error ? value.message : String(value);
1738
1757
  });
1739
1758
  }).catch((value) => {
1740
- if (this.stopping || this.targetUrl === void 0) return;
1759
+ if (this.stopping || this.targetUrl === void 0 || gen !== this.generation) return;
1741
1760
  const message = value instanceof Error ? value.message : String(value);
1742
1761
  this.fail(`could not obtain the cloudflared binary: ${message}`);
1743
1762
  });
@@ -1968,7 +1987,7 @@ async function fetchLatestVersion(name, fetchImpl, timeoutMs = 1e4) {
1968
1987
  controller.abort();
1969
1988
  }, timeoutMs);
1970
1989
  try {
1971
- const response = await fetchImpl("https://registry.npmjs.org/" + name.replace("/", "%2F") + "/latest");
1990
+ const response = await fetchImpl("https://registry.npmjs.org/" + name.replace("/", "%2F") + "/latest", { signal: controller.signal });
1972
1991
  if (!response.ok) return void 0;
1973
1992
  const body = await response.json();
1974
1993
  if (typeof body !== "object" || body === null) return void 0;
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.
@@ -13052,13 +13056,6 @@ function chunkTarget(data) {
13052
13056
  if (step !== void 0) result.step = step;
13053
13057
  return result;
13054
13058
  }
13055
- /** Max message seq across an existing list, or -1 for an empty list. */
13056
- function watermarkOf(existing) {
13057
- if (existing === void 0) return -1;
13058
- let maxSeq = -1;
13059
- for (const message of existing) if (message.seq > maxSeq) maxSeq = message.seq;
13060
- return maxSeq;
13061
- }
13062
13059
  function createState(existing) {
13063
13060
  const messages = existing === void 0 ? [] : [...existing];
13064
13061
  const state = {
@@ -13067,9 +13064,11 @@ function createState(existing) {
13067
13064
  pendingByTurnStep: /* @__PURE__ */ new Map(),
13068
13065
  turnStepMessage: /* @__PURE__ */ new Map(),
13069
13066
  messageTurn: /* @__PURE__ */ new Map(),
13070
- toolNames: /* @__PURE__ */ new Map()
13067
+ toolNames: /* @__PURE__ */ new Map(),
13068
+ maxSeq: -1
13071
13069
  };
13072
13070
  for (const message of messages) {
13071
+ if (message.seq > state.maxSeq) state.maxSeq = message.seq;
13073
13072
  state.byId.set(message.id, message);
13074
13073
  if (message.kind !== "assistant") continue;
13075
13074
  const decoded = decodePendingTurnStep(message.id);
@@ -13123,6 +13122,7 @@ function retargetTurnStep(state, key, oldMessage, next) {
13123
13122
  }
13124
13123
  /** Fold one event into the working state. Assumes the event passes the watermark. */
13125
13124
  function applyEvent(state, event) {
13125
+ if (event.seq > state.maxSeq) state.maxSeq = event.seq;
13126
13126
  switch (event.type) {
13127
13127
  case "user/message":
13128
13128
  applyUserMessage(state, event);
@@ -13417,14 +13417,58 @@ function applyTurnEnd(state, event) {
13417
13417
  * @returns messages sorted by seq.
13418
13418
  */
13419
13419
  function foldEvents(events, existing) {
13420
- const sorted = [...events].sort((a, b) => a.seq - b.seq);
13421
- const watermark = watermarkOf(existing);
13422
- const state = createState(existing);
13423
- for (const event of sorted) {
13424
- if (event.seq <= watermark) continue;
13425
- applyEvent(state, event);
13426
- }
13427
- 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];
13428
13472
  let ordered = true;
13429
13473
  for (let index = 1; index < out.length; index += 1) {
13430
13474
  const prev = out[index - 1];
@@ -13434,7 +13478,7 @@ function foldEvents(events, existing) {
13434
13478
  break;
13435
13479
  }
13436
13480
  }
13437
- 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));
13438
13482
  }
13439
13483
  //#endregion
13440
13484
  //#region src/mobile/markdown.ts
@@ -13911,6 +13955,8 @@ function ChatView({ session, mux, onBack }) {
13911
13955
  const tailLoadingRef = (0, import_react.useRef)(true);
13912
13956
  /** Live session events buffered while the initial tail page loads. */
13913
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);
13914
13960
  /** True once the live buffer hit its cap (oldest events were dropped). */
13915
13961
  const liveBufferOverflowRef = (0, import_react.useRef)(false);
13916
13962
  /** The session's permission select (absent = capability not composed). */
@@ -13946,6 +13992,7 @@ function ChatView({ session, mux, onBack }) {
13946
13992
  tailLoadingRef.current = true;
13947
13993
  liveBufferRef.current = [];
13948
13994
  liveBufferOverflowRef.current = false;
13995
+ folderRef.current = void 0;
13949
13996
  setLoading(true);
13950
13997
  setError(void 0);
13951
13998
  setMessages([]);
@@ -13954,14 +14001,18 @@ function ChatView({ session, mux, onBack }) {
13954
14001
  const buffered = liveBufferRef.current;
13955
14002
  liveBufferRef.current = [];
13956
14003
  tailLoadingRef.current = false;
13957
- 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));
13958
14007
  setHasOlder(page.hasMore);
13959
14008
  setLoading(false);
13960
14009
  const projections = page.projections?.values;
13961
14010
  setPermissions(parsePermissionSelect(projections?.["permissions"]));
13962
14011
  if (liveBufferOverflowRef.current) loadHistory(session.sessionId, void 0, controller.signal).then((fresh) => {
13963
14012
  if (cancelled) return;
13964
- 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));
13965
14016
  liveBufferOverflowRef.current = false;
13966
14017
  }, (reason) => {
13967
14018
  if (cancelled) return;
@@ -13972,7 +14023,10 @@ function ChatView({ session, mux, onBack }) {
13972
14023
  const buffered = liveBufferRef.current;
13973
14024
  liveBufferRef.current = [];
13974
14025
  tailLoadingRef.current = false;
13975
- 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
+ }
13976
14030
  setError(errorText(reason));
13977
14031
  setLoading(false);
13978
14032
  });
@@ -14002,7 +14056,10 @@ function ChatView({ session, mux, onBack }) {
14002
14056
  liveBufferRef.current.push(event);
14003
14057
  return;
14004
14058
  }
14005
- 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
+ });
14006
14063
  return;
14007
14064
  }
14008
14065
  if (frame.type === "session/projection" && frame.sessionId === session.sessionId && frame.key === "permissions") setPermissions(parsePermissionSelect(frame.value));
@@ -14039,7 +14096,12 @@ function ChatView({ session, mux, onBack }) {
14039
14096
  pendingRef.current = false;
14040
14097
  setLoading(false);
14041
14098
  const older = foldEvents(page.events.map(eventOf));
14042
- 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
+ }
14043
14105
  setHasOlder(page.hasMore);
14044
14106
  }, (reason) => {
14045
14107
  pendingRef.current = false;
@@ -14274,8 +14336,12 @@ function ChatView({ session, mux, onBack }) {
14274
14336
  ]
14275
14337
  });
14276
14338
  }
14277
- /** One rendered message row (user bubble or assistant bubble with folds). */
14278
- 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 }) {
14279
14345
  if (message.kind === "user" && message.sourceKind !== void 0 && message.sourceKind !== "user" && !showSystemMessages) return null;
14280
14346
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14281
14347
  className: `chat-msg chat-msg-${message.kind}${message.pending === true ? " chat-msg-pending" : ""}${message.failed === true ? " chat-msg-failed" : ""}`,
@@ -14296,7 +14362,7 @@ function MessageRow({ message, showToolCalls, showSystemMessages }) {
14296
14362
  })
14297
14363
  ]
14298
14364
  });
14299
- }
14365
+ });
14300
14366
  /** Collapsed-by-default reasoning disclosure (web-UI Think-row parity). */
14301
14367
  function ReasoningDisclosure({ text, pending }) {
14302
14368
  const [open, setOpen] = (0, import_react.useState)(false);
@@ -14848,6 +14914,9 @@ function SessionListView({ workspace, onBack, onPick }) {
14848
14914
  const [error, setError] = (0, import_react.useState)(void 0);
14849
14915
  const [creating, setCreating] = (0, import_react.useState)(false);
14850
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);
14851
14920
  const cursorRef = (0, import_react.useRef)(void 0);
14852
14921
  const busyRef = (0, import_react.useRef)(false);
14853
14922
  (0, import_react.useEffect)(() => {
@@ -14870,6 +14939,25 @@ function SessionListView({ workspace, onBack, onPick }) {
14870
14939
  cancelled = true;
14871
14940
  };
14872
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]);
14873
14961
  /** Pull the next page and append the rows that belong to this workspace. */
14874
14962
  const loadMore = (0, import_react.useCallback)(() => {
14875
14963
  if (busyRef.current) return;
@@ -14894,7 +14982,10 @@ function SessionListView({ workspace, onBack, onPick }) {
14894
14982
  if (creating) return;
14895
14983
  setCreating(true);
14896
14984
  setCreateError(void 0);
14897
- createSession({ workspaceId: workspace.workspaceId }).then((created) => {
14985
+ createSession({
14986
+ workspaceId: workspace.workspaceId,
14987
+ ...selectedPreset !== void 0 ? { agentPreset: selectedPreset } : {}
14988
+ }).then((created) => {
14898
14989
  setCreating(false);
14899
14990
  const view = {
14900
14991
  sessionId: created.sessionId,
@@ -14912,9 +15003,11 @@ function SessionListView({ workspace, onBack, onPick }) {
14912
15003
  }, [
14913
15004
  creating,
14914
15005
  workspace,
14915
- onPick
15006
+ onPick,
15007
+ selectedPreset
14916
15008
  ]);
14917
15009
  const createHint = createError !== void 0 ? staleHostHint(createError) : void 0;
15010
+ const selectedPresetEntry = presets.find((preset) => preset.id === selectedPreset);
14918
15011
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14919
15012
  className: "mobile",
14920
15013
  children: [
@@ -14939,17 +15032,50 @@ function SessionListView({ workspace, onBack, onPick }) {
14939
15032
  className: "mobile-error mobile-pad",
14940
15033
  children: error
14941
15034
  }),
14942
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
14943
- className: "mobile-pad",
14944
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
14945
- type: "button",
14946
- className: "mobile-new",
14947
- disabled: creating,
14948
- onClick: () => {
14949
- handleCreate();
14950
- },
14951
- children: creating ? "创建中…" : "+ 新建会话"
14952
- })
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
+ ]
14953
15079
  }),
14954
15080
  createError !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
14955
15081
  className: "mobile-error mobile-pad",
@@ -15022,6 +15148,7 @@ function SessionListView({ workspace, onBack, onPick }) {
15022
15148
  function WorkspaceView({ initialWorkspaceId, onPick }) {
15023
15149
  const [items, setItems] = (0, import_react.useState)(void 0);
15024
15150
  const [error, setError] = (0, import_react.useState)(void 0);
15151
+ const [reload, setReload] = (0, import_react.useState)(0);
15025
15152
  (0, import_react.useEffect)(() => {
15026
15153
  let cancelled = false;
15027
15154
  listWorkspaces().then((rows) => {
@@ -15039,7 +15166,11 @@ function WorkspaceView({ initialWorkspaceId, onPick }) {
15039
15166
  return () => {
15040
15167
  cancelled = true;
15041
15168
  };
15042
- }, [initialWorkspaceId, onPick]);
15169
+ }, [
15170
+ initialWorkspaceId,
15171
+ onPick,
15172
+ reload
15173
+ ]);
15043
15174
  if (error !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
15044
15175
  className: "mobile",
15045
15176
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
@@ -15059,6 +15190,7 @@ function WorkspaceView({ initialWorkspaceId, onPick }) {
15059
15190
  onClick: () => {
15060
15191
  setError(void 0);
15061
15192
  setItems(void 0);
15193
+ setReload((n) => n + 1);
15062
15194
  },
15063
15195
  children: "重试"
15064
15196
  })]
@@ -15707,6 +15839,43 @@ body {
15707
15839
  padding: 8px 16px;
15708
15840
  }
15709
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
+
15710
15879
  /* ── buttons ─────────────────────────────────────────────────────────── */
15711
15880
 
15712
15881
  .mobile-button {
@@ -16351,6 +16520,7 @@ body {
16351
16520
  .mobile-row:focus-visible,
16352
16521
  .mobile-button:focus-visible,
16353
16522
  .mobile-new:focus-visible,
16523
+ .mobile-presetSelect:focus-visible,
16354
16524
  .chat-send:focus-visible,
16355
16525
  .chat-chip:focus-visible,
16356
16526
  .chat-msg-toggle:focus-visible,