@linxin666/dsh-remote-web-ui 0.3.1 → 0.3.3

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 (65) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +22 -8
  3. package/README.zh.md +12 -7
  4. package/lib/client.js +174 -16
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +104 -7
  7. package/lib/mobile.js +62 -7
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/RemoteEntry.js +1 -1
  10. package/lib/types/client/UpdatePanel.d.ts.map +1 -1
  11. package/lib/types/client/UpdatePanel.js +20 -3
  12. package/lib/types/client/index.d.ts +1 -1
  13. package/lib/types/client/index.d.ts.map +1 -1
  14. package/lib/types/client/index.js +4 -0
  15. package/lib/types/client/locales.d.ts +12 -0
  16. package/lib/types/client/locales.d.ts.map +1 -1
  17. package/lib/types/client/locales.js +22 -10
  18. package/lib/types/client/settings-form.d.ts +1 -1
  19. package/lib/types/client/settings-form.js +1 -1
  20. package/lib/types/client/telemetry.d.ts +27 -0
  21. package/lib/types/client/telemetry.d.ts.map +1 -0
  22. package/lib/types/client/telemetry.js +97 -0
  23. package/lib/types/http.d.ts +1 -1
  24. package/lib/types/http.js +1 -1
  25. package/lib/types/index.d.ts.map +1 -1
  26. package/lib/types/index.js +14 -3
  27. package/lib/types/loopback.d.ts +1 -1
  28. package/lib/types/loopback.js +1 -1
  29. package/lib/types/mobile/api.d.ts +7 -0
  30. package/lib/types/mobile/api.d.ts.map +1 -1
  31. package/lib/types/mobile/api.js +7 -0
  32. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  33. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  34. package/lib/types/mobile/mobile-styles.js +10 -0
  35. package/lib/types/mobile/views/ChatView.d.ts +1 -0
  36. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  37. package/lib/types/mobile/views/ChatView.js +32 -5
  38. package/lib/types/mobile/views/ChatView.stream.test.js +28 -2
  39. package/lib/types/mobile/views/ChatView.test.js +152 -2
  40. package/lib/types/mobile-api.d.ts.map +1 -1
  41. package/lib/types/mobile-api.js +3 -0
  42. package/lib/types/mount-once.d.ts +1 -1
  43. package/lib/types/mount-once.js +2 -2
  44. package/lib/types/update.d.ts +35 -4
  45. package/lib/types/update.d.ts.map +1 -1
  46. package/lib/types/update.js +98 -4
  47. package/package.json +2 -2
  48. package/src/client/RemoteEntry.tsx +1 -1
  49. package/src/client/UpdatePanel.tsx +43 -3
  50. package/src/client/index.ts +6 -1
  51. package/src/client/locales.ts +22 -10
  52. package/src/client/remote.module.css +56 -3
  53. package/src/client/settings-form.ts +1 -1
  54. package/src/client/telemetry.ts +105 -0
  55. package/src/http.ts +1 -1
  56. package/src/index.ts +16 -2
  57. package/src/loopback.ts +1 -1
  58. package/src/mobile/api.ts +8 -0
  59. package/src/mobile/mobile-styles.ts +10 -0
  60. package/src/mobile/views/ChatView.stream.test.tsx +34 -2
  61. package/src/mobile/views/ChatView.test.tsx +177 -2
  62. package/src/mobile/views/ChatView.tsx +44 -6
  63. package/src/mobile-api.ts +2 -0
  64. package/src/mount-once.ts +2 -2
  65. package/src/update.ts +117 -4
package/lib/index.js CHANGED
@@ -1557,7 +1557,8 @@ const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
1557
1557
  "session.prompt",
1558
1558
  "session.models",
1559
1559
  "session.selectModel",
1560
- "session.rename"
1560
+ "session.rename",
1561
+ "session.cancel"
1561
1562
  ]);
1562
1563
  /**
1563
1564
  * Locally answered display-preference method (the phone's read-only
@@ -1874,6 +1875,7 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
1874
1875
  if (method === "session.models") return wrap(await apiProxy.sessions.models(request));
1875
1876
  if (method === "session.selectModel") return wrap(await apiProxy.sessions.selectModel(request));
1876
1877
  if (method === "session.rename") return wrap(await apiProxy.sessions.rename(request));
1878
+ if (method === "session.cancel") return wrap(await apiProxy.sessions.cancel(request));
1877
1879
  throw new Error(`unhandled allowlisted method ${method}`);
1878
1880
  }
1879
1881
  //#endregion
@@ -2952,9 +2954,11 @@ var TunnelManager = class {
2952
2954
  }
2953
2955
  };
2954
2956
  /** The aggregate package that is the canonical update entry point. */
2955
- const AGGREGATE_PACKAGE = "@linxin666/dsh-web-ui-all";
2957
+ const AGGREGATE_PACKAGE = "@linxin666/dsh-web-all";
2956
2958
  /** Fallback anchor: this plugin's own package when the aggregate is absent. */
2957
2959
  const SELF_PACKAGE = "@linxin666/dsh-remote-web-ui";
2960
+ /** GitHub repository used to surface human-readable release notes. */
2961
+ const UPDATE_RELEASE_REPO = "zhu1090093659/dsh-web";
2958
2962
  /** A profile manifest `name` prefix (e.g. `dsh-profile-web`). */
2959
2963
  const PROFILE_NAME_PREFIX = "dsh-profile-";
2960
2964
  /** How many ancestor directories a profile search walks before giving up. */
@@ -3135,6 +3139,83 @@ async function fetchLatestVersion(name, fetchImpl, timeoutMs = 1e4) {
3135
3139
  return;
3136
3140
  }
3137
3141
  }
3142
+ /** Normalize one release-note bullet. */
3143
+ function cleanNoteItem(text) {
3144
+ return text.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/^[-*]\s+/, "").trim();
3145
+ }
3146
+ /** Determine the section kind for a Markdown heading line. */
3147
+ function noteSectionOf(line) {
3148
+ const heading = /^#{1,6}\s+(.+)$/.exec(line.trim());
3149
+ if (heading === null) return void 0;
3150
+ const title = heading[1].replace(/[::].*$/, "").trim().toLowerCase();
3151
+ if (title === "新功能" || title === "new features") return "features";
3152
+ if (title === "修复" || title === "bug fixes" || title === "bugfixes") return "fixes";
3153
+ if (title === "其他改动" || title === "other changes") return "other";
3154
+ }
3155
+ /**
3156
+ * Parse the GitHub Release body into the three conventional sections.
3157
+ *
3158
+ * The committed release notes are bilingual: Chinese is the default view and
3159
+ * English lives inside a `<details>` block. Prefer the Chinese sections when
3160
+ * present and fall back to the whole body for English-only releases. Bullets
3161
+ * are escaped as plain text for the panel (no Markdown renderer).
3162
+ */
3163
+ function parseReleaseNotesSource(version, source) {
3164
+ const notes = {
3165
+ version,
3166
+ features: [],
3167
+ fixes: [],
3168
+ other: []
3169
+ };
3170
+ let current;
3171
+ for (const rawLine of source.split(/\r?\n/)) {
3172
+ const line = rawLine.trim();
3173
+ const section = noteSectionOf(line);
3174
+ if (section !== void 0) {
3175
+ current = section;
3176
+ continue;
3177
+ }
3178
+ if (current === void 0 || !/^-\s+/.test(line)) continue;
3179
+ const item = cleanNoteItem(line);
3180
+ if (item !== "") notes[current].push(item);
3181
+ }
3182
+ if (notes.features.length === 0 && notes.fixes.length === 0 && notes.other.length === 0) return void 0;
3183
+ return notes;
3184
+ }
3185
+ function parseReleaseNotesBody(version, body) {
3186
+ return parseReleaseNotesSource(version, body.split("<details>")[0] ?? body) ?? parseReleaseNotesSource(version, body);
3187
+ }
3188
+ /**
3189
+ * Fetch and parse the GitHub Release for one `dsh-web` version.
3190
+ * @param version - the npm release version (without the `v` prefix).
3191
+ * @param fetchImpl - injected fetch; global `fetch` in the host.
3192
+ * @param timeoutMs - hard timeout for the GitHub API probe.
3193
+ * @returns structured notes, or undefined when unavailable.
3194
+ */
3195
+ async function fetchGitHubReleaseNotes(version, fetchImpl = fetch, timeoutMs = 1e4) {
3196
+ const controller = new AbortController();
3197
+ const timer = setTimeout(() => {
3198
+ controller.abort();
3199
+ }, timeoutMs);
3200
+ try {
3201
+ const response = await fetchImpl(`https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`, {
3202
+ headers: {
3203
+ accept: "application/vnd.github+json",
3204
+ "user-agent": "dsh-web-update"
3205
+ },
3206
+ signal: controller.signal
3207
+ });
3208
+ if (!response.ok) return void 0;
3209
+ const body = await response.json();
3210
+ if (typeof body !== "object" || body === null) return void 0;
3211
+ const text = body.body;
3212
+ return typeof text === "string" ? parseReleaseNotesBody(version, text) : void 0;
3213
+ } catch {
3214
+ return;
3215
+ } finally {
3216
+ clearTimeout(timer);
3217
+ }
3218
+ }
3138
3219
  /**
3139
3220
  * Sentinel for an installed version that could not be read (missing manifest
3140
3221
  * or resolve failure). It is a real-looking version so `checkUpdates` can
@@ -3202,12 +3283,15 @@ async function checkUpdates(deps) {
3202
3283
  });
3203
3284
  });
3204
3285
  const error = probeFailures === names.length && names.length > 0 ? "registry-unreachable" : void 0;
3286
+ const targetVersion = packages.find((packageStatus) => packageStatus.latest !== void 0)?.latest;
3287
+ const notes = targetVersion !== void 0 && deps.fetchReleaseNotes !== void 0 ? await deps.fetchReleaseNotes(targetVersion).catch(() => void 0) : void 0;
3205
3288
  return {
3206
3289
  mode: linked ? "link" : "npm",
3207
3290
  profileName: profile.name,
3208
3291
  anchor,
3209
3292
  packages,
3210
3293
  outdated: packages.some((packageStatus) => packageStatus.outdated),
3294
+ ...notes !== void 0 ? { notes } : {},
3211
3295
  ...error !== void 0 ? { error } : {}
3212
3296
  };
3213
3297
  }
@@ -3497,7 +3581,7 @@ function makeUpdateRoutes(deps) {
3497
3581
  //#region src/mount-once.ts
3498
3582
  /**
3499
3583
  * Host single-instance guard shared by the plugin family. The family bundle
3500
- * (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
3584
+ * (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
3501
3585
  * the loader accepts a standalone install of the same package side by side;
3502
3586
  * without this guard the second instance would still re-register the same
3503
3587
  * webserver routes, tools, settings namespaces, and system-prompt sections
@@ -3510,7 +3594,7 @@ function makeUpdateRoutes(deps) {
3510
3594
  * `ctx.effect` runs its callback immediately and treats the callback's
3511
3595
  * return value as the fiber disposer, so the unmarker is returned, not run.
3512
3596
  */
3513
- const MOUNTED = Symbol.for("dsh-web-ui.mounted-plugins");
3597
+ const MOUNTED = Symbol.for("dsh-web.mounted-plugins");
3514
3598
  function mountedSet() {
3515
3599
  const registry = globalThis;
3516
3600
  return registry[MOUNTED] ??= /* @__PURE__ */ new Set();
@@ -3765,6 +3849,17 @@ function applyImpl(ctx, config) {
3765
3849
  return;
3766
3850
  }
3767
3851
  });
3852
+ const releaseNotesCache = /* @__PURE__ */ new Map();
3853
+ const fetchReleaseNotesCached = async (version) => {
3854
+ const cached = releaseNotesCache.get(version);
3855
+ if (cached !== void 0 && Date.now() - cached.at < 6e5) return cached.notes;
3856
+ const notes = await fetchGitHubReleaseNotes(version, fetch);
3857
+ releaseNotesCache.set(version, {
3858
+ at: Date.now(),
3859
+ notes
3860
+ });
3861
+ return notes;
3862
+ };
3768
3863
  const updateRoutes = makeUpdateRoutes({
3769
3864
  fence: (request) => isTrustedApiRequest(request, []),
3770
3865
  check: () => checkUpdates({
@@ -3776,7 +3871,8 @@ function applyImpl(ctx, config) {
3776
3871
  return;
3777
3872
  }
3778
3873
  },
3779
- fetchLatest: (name) => fetchLatestVersion(name, fetch)
3874
+ fetchLatest: (name) => fetchLatestVersion(name, fetch),
3875
+ fetchReleaseNotes: fetchReleaseNotesCached
3780
3876
  }),
3781
3877
  run: async () => {
3782
3878
  const target = resolveUpdateTarget({ anchorManifestPath: resolveAnchorPath() });
@@ -3786,7 +3882,7 @@ function applyImpl(ctx, config) {
3786
3882
  ok: false,
3787
3883
  exitCode: null,
3788
3884
  output: "",
3789
- error: code === "not-found" ? "dsh-web-ui aggregate not installed" : "local link install — update unavailable",
3885
+ error: code === "not-found" ? "dsh-web aggregate not installed" : "local link install — update unavailable",
3790
3886
  errorCode: code
3791
3887
  };
3792
3888
  }
@@ -3804,7 +3900,8 @@ function applyImpl(ctx, config) {
3804
3900
  return;
3805
3901
  }
3806
3902
  },
3807
- fetchLatest: (name) => fetchLatestVersion(name, fetch)
3903
+ fetchLatest: (name) => fetchLatestVersion(name, fetch),
3904
+ fetchReleaseNotes: fetchReleaseNotesCached
3808
3905
  }
3809
3906
  });
3810
3907
  }
package/lib/mobile.js CHANGED
@@ -7619,6 +7619,13 @@ async function sendCommand(sessionId, line) {
7619
7619
  }]
7620
7620
  });
7621
7621
  }
7622
+ /**
7623
+ * Stop the session's active turn (the mobile stop button). Pending queued
7624
+ * work is preserved and resumes in FIFO order once cancellation settles.
7625
+ */
7626
+ async function cancelSession(sessionId) {
7627
+ return await callUnary("session.cancel", { sessionId });
7628
+ }
7622
7629
  /** Fresh advisory model directory for one session (current + groups + failures). */
7623
7630
  async function models(sessionId) {
7624
7631
  return await callUnary("session.models", { sessionId });
@@ -14039,6 +14046,8 @@ function ChatView({ session, mux, onBack }) {
14039
14046
  const [mobileEnterToSend, setMobileEnterToSend] = (0, import_react.useState)(true);
14040
14047
  /** Whether the assistant is currently generating (turn/start..turn/end). */
14041
14048
  const [running, setRunning] = (0, import_react.useState)(false);
14049
+ /** Whether a stop request is in flight (guards the composer's stop button). */
14050
+ const [stopping, setStopping] = (0, import_react.useState)(false);
14042
14051
  /** Pending tool approvals awaiting user decision (#1025). */
14043
14052
  const [pendingApprovals, setPendingApprovals] = (0, import_react.useState)([]);
14044
14053
  /** Pending questions awaiting user answer (#1025). */
@@ -14245,6 +14254,22 @@ function ChatView({ session, mux, onBack }) {
14245
14254
  sending,
14246
14255
  session.sessionId
14247
14256
  ]);
14257
+ /**
14258
+ * Stop the active turn (desktop parity: the composer's primary button
14259
+ * becomes a stop button while running). The turn/end frame arriving over
14260
+ * mux flips the button back; a failed request surfaces through the chat
14261
+ * error line.
14262
+ */
14263
+ const stopTurn = (0, import_react.useCallback)(() => {
14264
+ if (stopping) return;
14265
+ setStopping(true);
14266
+ cancelSession(session.sessionId).then(() => {
14267
+ setStopping(false);
14268
+ }, (reason) => {
14269
+ setStopping(false);
14270
+ setError(errorText(reason));
14271
+ });
14272
+ }, [stopping, session.sessionId]);
14248
14273
  const modelLabel = currentModel?.model ?? "模型";
14249
14274
  const permissionLabel = permissions === void 0 ? void 0 : permissions.options.find((option) => option.value === permissions.currentValue)?.name ?? displayName(permissions.currentValue);
14250
14275
  const contextUsage = (0, import_react.useMemo)(() => {
@@ -14434,12 +14459,28 @@ function ChatView({ session, mux, onBack }) {
14434
14459
  }
14435
14460
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
14436
14461
  type: "button",
14437
- className: "chat-send",
14438
- disabled: sending || input.trim() === "",
14462
+ className: running ? "chat-send chat-send-stop" : "chat-send",
14463
+ ...running ? { "aria-label": stopping ? "停止中" : "停止" } : {},
14464
+ disabled: running ? stopping : sending || input.trim() === "",
14439
14465
  onClick: () => {
14440
- send();
14466
+ if (running) stopTurn();
14467
+ else send();
14441
14468
  },
14442
- children: sending ? "发送中…" : "发送"
14469
+ children: running ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", {
14470
+ viewBox: "0 0 16 16",
14471
+ width: "14",
14472
+ height: "14",
14473
+ "aria-hidden": "true",
14474
+ focusable: "false",
14475
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", {
14476
+ x: "3",
14477
+ y: "3",
14478
+ width: "10",
14479
+ height: "10",
14480
+ rx: "3",
14481
+ fill: "currentColor"
14482
+ })
14483
+ }) : sending ? "发送中…" : "发送"
14443
14484
  })]
14444
14485
  }),
14445
14486
  sheet === "model" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModelSheet, {
@@ -14490,6 +14531,11 @@ function ChatView({ session, mux, onBack }) {
14490
14531
  */
14491
14532
  const MessageRow = (0, import_react.memo)(function MessageRow({ message, showToolCalls, showSystemMessages }) {
14492
14533
  if (message.kind === "user" && message.sourceKind !== void 0 && message.sourceKind !== "user" && !showSystemMessages) return null;
14534
+ const hasReasoning = message.kind === "assistant" && message.reasoning !== void 0 && message.reasoning !== "";
14535
+ const hasTools = showToolCalls && message.kind === "assistant" && message.tools !== void 0 && message.tools.length > 0;
14536
+ const hasText = message.text !== "";
14537
+ const hasFailTag = message.failed === true;
14538
+ if (!hasReasoning && !hasTools && !hasText && !hasFailTag) return null;
14493
14539
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14494
14540
  className: `chat-msg chat-msg-${message.kind}${message.pending === true ? " chat-msg-pending" : ""}${message.failed === true ? " chat-msg-failed" : ""}`,
14495
14541
  children: [
@@ -14648,7 +14694,7 @@ function MarkdownText({ text, pending }) {
14648
14694
  if (timerRef.current !== void 0) clearTimeout(timerRef.current);
14649
14695
  };
14650
14696
  }, []);
14651
- const long = text.length > LONG_TEXT_LIMIT;
14697
+ const long = !pending && text.length > 6e3;
14652
14698
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14653
14699
  className: "chat-msg-text chat-md" + (long && !open ? " chat-md-collapsed" : ""),
14654
14700
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
@@ -14667,7 +14713,7 @@ function MarkdownText({ text, pending }) {
14667
14713
  /** Long assistant text collapses behind an explicit expand toggle. */
14668
14714
  function CollapsibleText({ text }) {
14669
14715
  const [open, setOpen] = (0, import_react.useState)(false);
14670
- if (text.length <= LONG_TEXT_LIMIT) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14716
+ if (text.length <= 6e3) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14671
14717
  className: "chat-msg-text",
14672
14718
  children: text
14673
14719
  });
@@ -14687,7 +14733,6 @@ function CollapsibleText({ text }) {
14687
14733
  ]
14688
14734
  });
14689
14735
  }
14690
- const LONG_TEXT_LIMIT = 1600;
14691
14736
  const LONG_TEXT_PREVIEW = 800;
14692
14737
  /** Latest non-empty line of a streaming reasoning buffer. */
14693
14738
  function lastLine(text) {
@@ -17270,6 +17315,16 @@ body {
17270
17315
  opacity: 0.5;
17271
17316
  }
17272
17317
 
17318
+ /* Running state: the primary button carries only the square stop icon, so
17319
+ it centers the glyph instead of sizing to text. */
17320
+ .chat-send-stop {
17321
+ display: inline-flex;
17322
+ align-items: center;
17323
+ justify-content: center;
17324
+ min-width: 44px;
17325
+ padding: 0 12px;
17326
+ }
17327
+
17273
17328
  .chat-load-older {
17274
17329
  align-self: center;
17275
17330
  margin: 4px 0;