@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/mobile.js CHANGED
@@ -7570,6 +7570,10 @@ async function listWorkspaces() {
7570
7570
  const { items } = await callUnary("workspace.list", {});
7571
7571
  return items;
7572
7572
  }
7573
+ /** Read-only mobile display preferences (answered by the plugin, not the host proxy). */
7574
+ async function fetchMobilePreferences() {
7575
+ return await callUnary("mobile.preferences", {});
7576
+ }
7573
7577
  /** One session.list page; omit the cursor for the first page. */
7574
7578
  async function listSessions(cursor) {
7575
7579
  return await callUnary("session.list", cursor === void 0 ? {} : { cursor });
@@ -7581,13 +7585,13 @@ async function listSessions(cursor) {
7581
7585
  async function createSession(options = {}) {
7582
7586
  return await callUnary("session.create", options);
7583
7587
  }
7584
- /** One history window; omit beforeSeq for the tail page. */
7585
- async function history(sessionId, beforeSeq, maxMessages = 30) {
7588
+ /** One history window; omit beforeSeq for the tail page, pass a signal to abort. */
7589
+ async function history(sessionId, beforeSeq, maxMessages = 30, signal) {
7586
7590
  return await callUnary("session.history", {
7587
7591
  sessionId,
7588
7592
  maxMessages,
7589
7593
  ...beforeSeq !== void 0 ? { beforeSeq } : {}
7590
- });
7594
+ }, signal);
7591
7595
  }
7592
7596
  /** Send one text prompt (queued: the agent picks it up in order). */
7593
7597
  async function prompt(sessionId, text) {
@@ -13526,20 +13530,6 @@ function ThemeToggle() {
13526
13530
  children: dark ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SunIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MoonIcon, {})
13527
13531
  });
13528
13532
  }
13529
- //#endregion
13530
- //#region src/mobile/views/ChatView.tsx
13531
- /**
13532
- * Chat level: one session. Loads the history tail page on open, appends
13533
- * pages upward (loadOlder), folds live mux frames in as they arrive, and
13534
- * sends prompts through session.prompt.
13535
- *
13536
- * Rendering mirrors the desktop web UI's fold discipline on a small screen:
13537
- * - reasoning text hides behind a collapsed "深度思考" disclosure,
13538
- * - tool calls behind a collapsed tool disclosure (name + arguments),
13539
- * - very long assistant text collapses with an explicit expand toggle,
13540
- * - a toolbar above the composer carries the model (+ thinking effort) and
13541
- * permission pickers, both as bottom sheets.
13542
- */
13543
13533
  /** Extract the raw event from one history entry (the fold consumes events only). */
13544
13534
  function eventOf(entry) {
13545
13535
  return entry.event;
@@ -13597,26 +13587,74 @@ function ChatView({ session, mux, onBack }) {
13597
13587
  const [sending, setSending] = (0, import_react.useState)(false);
13598
13588
  const scrollRef = (0, import_react.useRef)(void 0);
13599
13589
  const pendingRef = (0, import_react.useRef)(false);
13590
+ /**
13591
+ * True while the initial tail page is in flight. Live events arriving in
13592
+ * that window go to {@link liveBufferRef} instead of the message list: the
13593
+ * tail load replaces the list wholesale, so a directly folded event would
13594
+ * flash once, be discarded by the snapshot, and then be skipped forever by
13595
+ * the seq watermark.
13596
+ */
13597
+ const tailLoadingRef = (0, import_react.useRef)(true);
13598
+ /** Live session events buffered while the initial tail page loads. */
13599
+ const liveBufferRef = (0, import_react.useRef)([]);
13600
+ /** True once the live buffer hit its cap (oldest events were dropped). */
13601
+ const liveBufferOverflowRef = (0, import_react.useRef)(false);
13600
13602
  /** The session's permission select (absent = capability not composed). */
13601
13603
  const [permissions, setPermissions] = (0, import_react.useState)(void 0);
13602
13604
  /** The current model selection for the toolbar chip (best-effort label). */
13603
13605
  const [currentModel, setCurrentModel] = (0, import_react.useState)(void 0);
13604
13606
  /** Which bottom sheet is open. */
13605
13607
  const [sheet, setSheet] = (0, import_react.useState)(null);
13608
+ /**
13609
+ * Composer preference from the plugin's host settings (default true keeps
13610
+ * the legacy Enter-to-send behavior until the preference loads).
13611
+ */
13612
+ const [mobileEnterToSend, setMobileEnterToSend] = (0, import_react.useState)(true);
13613
+ (0, import_react.useEffect)(() => {
13614
+ let cancelled = false;
13615
+ fetchMobilePreferences().then((preferences) => {
13616
+ if (!cancelled) setMobileEnterToSend(preferences.mobileEnterToSend !== false);
13617
+ }, () => {});
13618
+ return () => {
13619
+ cancelled = true;
13620
+ };
13621
+ }, []);
13606
13622
  (0, import_react.useEffect)(() => {
13607
13623
  let cancelled = false;
13624
+ const controller = new AbortController();
13625
+ const timeout = setTimeout(() => {
13626
+ controller.abort(new DOMException("history load timed out", "TimeoutError"));
13627
+ }, 15e3);
13628
+ tailLoadingRef.current = true;
13629
+ liveBufferRef.current = [];
13630
+ liveBufferOverflowRef.current = false;
13608
13631
  setLoading(true);
13609
13632
  setError(void 0);
13610
13633
  setMessages([]);
13611
- loadHistory(session.sessionId).then((page) => {
13634
+ loadHistory(session.sessionId, void 0, controller.signal).then((page) => {
13612
13635
  if (cancelled) return;
13613
- setMessages(foldEvents(page.events.map(eventOf)));
13636
+ const buffered = liveBufferRef.current;
13637
+ liveBufferRef.current = [];
13638
+ tailLoadingRef.current = false;
13639
+ setMessages(foldEvents(buffered, foldEvents(page.events.map(eventOf))));
13614
13640
  setHasOlder(page.hasMore);
13615
13641
  setLoading(false);
13616
13642
  const projections = page.projections?.values;
13617
13643
  setPermissions(parsePermissionSelect(projections?.["permissions"]));
13644
+ if (liveBufferOverflowRef.current) loadHistory(session.sessionId, void 0, controller.signal).then((fresh) => {
13645
+ if (cancelled) return;
13646
+ setMessages((previous) => foldEvents(fresh.events.map(eventOf), previous));
13647
+ liveBufferOverflowRef.current = false;
13648
+ }, (reason) => {
13649
+ if (cancelled) return;
13650
+ console.warn("history tail re-pull after live buffer overflow failed", reason);
13651
+ });
13618
13652
  }, (reason) => {
13619
13653
  if (cancelled) return;
13654
+ const buffered = liveBufferRef.current;
13655
+ liveBufferRef.current = [];
13656
+ tailLoadingRef.current = false;
13657
+ if (buffered.length > 0) setMessages(foldEvents(buffered));
13620
13658
  setError(errorText(reason));
13621
13659
  setLoading(false);
13622
13660
  });
@@ -13625,6 +13663,8 @@ function ChatView({ session, mux, onBack }) {
13625
13663
  }, () => {});
13626
13664
  return () => {
13627
13665
  cancelled = true;
13666
+ clearTimeout(timeout);
13667
+ controller.abort();
13628
13668
  };
13629
13669
  }, [session.sessionId]);
13630
13670
  (0, import_react.useEffect)(() => {
@@ -13632,7 +13672,19 @@ function ChatView({ session, mux, onBack }) {
13632
13672
  return mux.onFrame((frame) => {
13633
13673
  if (frame.type === "session/event") {
13634
13674
  if (frame.sessionId !== session.sessionId) return;
13635
- setMessages((previous) => foldEvents([frame.event], previous));
13675
+ const event = frame.event;
13676
+ if (tailLoadingRef.current) {
13677
+ if (liveBufferRef.current.length >= 500) {
13678
+ liveBufferRef.current.shift();
13679
+ if (!liveBufferOverflowRef.current) {
13680
+ console.warn(`history tail is slow: live buffer reached 500 events; oldest buffered events will be re-fetched`);
13681
+ liveBufferOverflowRef.current = true;
13682
+ }
13683
+ }
13684
+ liveBufferRef.current.push(event);
13685
+ return;
13686
+ }
13687
+ setMessages((previous) => foldEvents([event], previous));
13636
13688
  return;
13637
13689
  }
13638
13690
  if (frame.type === "session/projection" && frame.sessionId === session.sessionId && frame.key === "permissions") setPermissions(parsePermissionSelect(frame.value));
@@ -13800,13 +13852,13 @@ function ChatView({ session, mux, onBack }) {
13800
13852
  className: "chat-input",
13801
13853
  rows: 1,
13802
13854
  value: input,
13803
- placeholder: "输入消息,Enter 发送…",
13804
- enterKeyHint: "send",
13855
+ placeholder: mobileEnterToSend ? "输入消息,Enter 发送…" : "输入消息,Enter 换行,点按钮发送…",
13856
+ enterKeyHint: mobileEnterToSend ? "send" : "enter",
13805
13857
  onChange: (event) => {
13806
13858
  setInput(event.target.value);
13807
13859
  },
13808
13860
  onKeyDown: (event) => {
13809
- if (event.key === "Enter" && !event.shiftKey) {
13861
+ if (mobileEnterToSend && event.key === "Enter" && !event.shiftKey) {
13810
13862
  event.preventDefault();
13811
13863
  send();
13812
13864
  }
@@ -14503,13 +14555,18 @@ function SessionListView({ workspace, onBack, onPick }) {
14503
14555
  * @param props - the pick action.
14504
14556
  * @returns the roster.
14505
14557
  */
14506
- function WorkspaceView({ onPick }) {
14558
+ function WorkspaceView({ initialWorkspaceId, onPick }) {
14507
14559
  const [items, setItems] = (0, import_react.useState)(void 0);
14508
14560
  const [error, setError] = (0, import_react.useState)(void 0);
14509
14561
  (0, import_react.useEffect)(() => {
14510
14562
  let cancelled = false;
14511
14563
  listWorkspaces().then((rows) => {
14512
14564
  if (cancelled) return;
14565
+ const target = initialWorkspaceId === void 0 ? void 0 : rows.find((workspace) => workspace.workspaceId === initialWorkspaceId);
14566
+ if (target !== void 0) {
14567
+ onPick(target);
14568
+ return;
14569
+ }
14513
14570
  setItems(rows);
14514
14571
  }, (reason) => {
14515
14572
  if (cancelled) return;
@@ -14518,7 +14575,7 @@ function WorkspaceView({ onPick }) {
14518
14575
  return () => {
14519
14576
  cancelled = true;
14520
14577
  };
14521
- }, []);
14578
+ }, [initialWorkspaceId, onPick]);
14522
14579
  if (error !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14523
14580
  className: "mobile",
14524
14581
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
@@ -14617,6 +14674,11 @@ function WorkspaceView({ onPick }) {
14617
14674
  * router, no state library: the surface is three fixed levels with a back
14618
14675
  * affordance, and every piece of data is fetched on demand.
14619
14676
  */
14677
+ /** Read the optional workspace target carried from the pairing QR flow. */
14678
+ function mobileWorkspaceTarget(search) {
14679
+ const value = new URLSearchParams(search).get("workspace");
14680
+ return value === null || value === "" ? void 0 : value;
14681
+ }
14620
14682
  /** Map a list row to the surface model; the title comes from projections when present. */
14621
14683
  function toSessionView(item) {
14622
14684
  const titleValue = item.projections?.values?.title;
@@ -14647,6 +14709,7 @@ function formatTime(epochMs) {
14647
14709
  */
14648
14710
  function App() {
14649
14711
  const [route, setRoute] = (0, import_react.useState)({ kind: "workspaces" });
14712
+ const [initialWorkspaceId, setInitialWorkspaceId] = (0, import_react.useState)(() => mobileWorkspaceTarget(window.location.search));
14650
14713
  const muxRef = (0, import_react.useRef)(void 0);
14651
14714
  (0, import_react.useEffect)(() => {
14652
14715
  const mux = new MuxClient();
@@ -14676,14 +14739,24 @@ function App() {
14676
14739
  workspace
14677
14740
  });
14678
14741
  }, []);
14742
+ const openWorkspace = (0, import_react.useCallback)((workspace) => {
14743
+ if (initialWorkspaceId !== void 0) {
14744
+ const url = new URL(window.location.href);
14745
+ url.searchParams.delete("workspace");
14746
+ window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
14747
+ setInitialWorkspaceId(void 0);
14748
+ }
14749
+ setRoute({
14750
+ kind: "sessions",
14751
+ workspace
14752
+ });
14753
+ }, [initialWorkspaceId]);
14679
14754
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
14680
14755
  className: "mobile",
14681
- children: route.kind === "workspaces" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceView, { onPick: (workspace) => {
14682
- setRoute({
14683
- kind: "sessions",
14684
- workspace
14685
- });
14686
- } }) : route.kind === "sessions" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SessionListView, {
14756
+ children: route.kind === "workspaces" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceView, {
14757
+ initialWorkspaceId,
14758
+ onPick: openWorkspace
14759
+ }) : route.kind === "sessions" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SessionListView, {
14687
14760
  workspace: route.workspace,
14688
14761
  onBack: back,
14689
14762
  onPick: (session) => {
@@ -14714,8 +14787,8 @@ function staleHostHint(message) {
14714
14787
  return /^HTTP 403/.test(message) ? "宿主端插件可能仍在运行旧版本:请重启 dsh web 后再试。" : void 0;
14715
14788
  }
14716
14789
  /** Fetch one history page (tail by default) — thin wrapper so views share the call shape. */
14717
- function loadHistory(sessionId, beforeSeq) {
14718
- return history(sessionId, beforeSeq);
14790
+ function loadHistory(sessionId, beforeSeq, signal) {
14791
+ return history(sessionId, beforeSeq, void 0, signal);
14719
14792
  }
14720
14793
  //#endregion
14721
14794
  //#region src/mobile/mobile-styles.ts
@@ -15486,6 +15559,9 @@ body {
15486
15559
  min-width: 0;
15487
15560
  min-height: 40px;
15488
15561
  max-height: 120px;
15562
+ /* Grow with the content (still bounded by min/max-height); browsers
15563
+ without field-sizing support ignore this line and keep the old size. */
15564
+ field-sizing: content;
15489
15565
  padding: 9px 12px;
15490
15566
  border: 1px solid var(--m-border);
15491
15567
  border-radius: 10px;