@linxin666/dsh-remote-web-ui 0.3.2 → 0.3.4

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 (78) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +22 -8
  3. package/README.zh.md +12 -7
  4. package/assets/mobile-service-worker.js +1 -2
  5. package/lib/client.js +177 -19
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +222 -15
  8. package/lib/mobile.js +64 -13
  9. package/lib/mobile.js.map +1 -1
  10. package/lib/types/client/PluginSettingsCard.d.ts +2 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.js +2 -2
  13. package/lib/types/client/RemoteEntry.js +1 -1
  14. package/lib/types/client/UpdatePanel.d.ts.map +1 -1
  15. package/lib/types/client/UpdatePanel.js +20 -3
  16. package/lib/types/client/index.d.ts +1 -1
  17. package/lib/types/client/index.d.ts.map +1 -1
  18. package/lib/types/client/index.js +4 -0
  19. package/lib/types/client/locales.d.ts +12 -0
  20. package/lib/types/client/locales.d.ts.map +1 -1
  21. package/lib/types/client/locales.js +22 -10
  22. package/lib/types/client/settings-form.d.ts +1 -1
  23. package/lib/types/client/settings-form.js +1 -1
  24. package/lib/types/client/telemetry.d.ts +27 -0
  25. package/lib/types/client/telemetry.d.ts.map +1 -0
  26. package/lib/types/client/telemetry.js +97 -0
  27. package/lib/types/dsh-home.d.ts.map +1 -1
  28. package/lib/types/dsh-home.js +9 -3
  29. package/lib/types/http.d.ts +1 -1
  30. package/lib/types/http.js +1 -1
  31. package/lib/types/index.d.ts.map +1 -1
  32. package/lib/types/index.js +34 -5
  33. package/lib/types/loopback.d.ts +1 -1
  34. package/lib/types/loopback.js +1 -1
  35. package/lib/types/mobile/api.d.ts +7 -0
  36. package/lib/types/mobile/api.d.ts.map +1 -1
  37. package/lib/types/mobile/api.js +9 -3
  38. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  39. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  40. package/lib/types/mobile/mobile-styles.js +10 -0
  41. package/lib/types/mobile/views/ChatView.d.ts +1 -0
  42. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  43. package/lib/types/mobile/views/ChatView.js +32 -5
  44. package/lib/types/mobile/views/ChatView.stream.test.js +28 -2
  45. package/lib/types/mobile/views/ChatView.test.js +152 -2
  46. package/lib/types/mobile-api.d.ts +14 -0
  47. package/lib/types/mobile-api.d.ts.map +1 -1
  48. package/lib/types/mobile-api.js +56 -1
  49. package/lib/types/mobile-routes.d.ts.map +1 -1
  50. package/lib/types/mobile-routes.js +25 -2
  51. package/lib/types/mount-once.d.ts +1 -1
  52. package/lib/types/mount-once.js +2 -2
  53. package/lib/types/update.d.ts +35 -4
  54. package/lib/types/update.d.ts.map +1 -1
  55. package/lib/types/update.js +98 -4
  56. package/package.json +4 -2
  57. package/src/client/PluginSettingsCard.tsx +4 -2
  58. package/src/client/RemoteEntry.tsx +1 -1
  59. package/src/client/UpdatePanel.tsx +43 -3
  60. package/src/client/index.ts +6 -1
  61. package/src/client/locales.ts +22 -10
  62. package/src/client/remote.module.css +56 -3
  63. package/src/client/settings-card.module.css +5 -0
  64. package/src/client/settings-form.ts +1 -1
  65. package/src/client/telemetry.ts +105 -0
  66. package/src/dsh-home.ts +9 -3
  67. package/src/http.ts +1 -1
  68. package/src/index.ts +36 -4
  69. package/src/loopback.ts +1 -1
  70. package/src/mobile/api.ts +10 -3
  71. package/src/mobile/mobile-styles.ts +10 -0
  72. package/src/mobile/views/ChatView.stream.test.tsx +34 -2
  73. package/src/mobile/views/ChatView.test.tsx +177 -2
  74. package/src/mobile/views/ChatView.tsx +44 -6
  75. package/src/mobile-api.ts +61 -0
  76. package/src/mobile-routes.ts +24 -2
  77. package/src/mount-once.ts +2 -2
  78. package/src/update.ts +117 -4
@@ -45,6 +45,7 @@ const MOBILE_ALLOWLIST = new Set([
45
45
  'session.models',
46
46
  'session.selectModel',
47
47
  'session.rename',
48
+ 'session.cancel',
48
49
  ]);
49
50
  /**
50
51
  * Locally answered display-preference method (the phone's read-only
@@ -54,6 +55,8 @@ const MOBILE_ALLOWLIST = new Set([
54
55
  const MOBILE_PREFERENCES_METHOD = 'mobile.preferences';
55
56
  const MOBILE_PENDING_METHOD = 'mobile.pending';
56
57
  const MOBILE_RESPOND_METHOD = 'mobile.respond';
58
+ /** Execute a slash command through the host command registry (#1125). */
59
+ const MOBILE_COMMAND_METHOD = 'mobile.command';
57
60
  /** One session.list page (thin phones load incrementally). */
58
61
  const SESSION_PAGE_SIZE = 20;
59
62
  /** SSE keep-alive ping cadence for the live mux stream (single connection). */
@@ -129,7 +132,8 @@ export function makeMobileApiRoutes(deps) {
129
132
  const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length);
130
133
  const local = method === MOBILE_PREFERENCES_METHOD
131
134
  || method === MOBILE_PENDING_METHOD
132
- || method === MOBILE_RESPOND_METHOD;
135
+ || method === MOBILE_RESPOND_METHOD
136
+ || method === MOBILE_COMMAND_METHOD;
133
137
  if (!MOBILE_ALLOWLIST.has(method) && !local) {
134
138
  writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: `method ${method} is not exposed to the mobile surface` } });
135
139
  return;
@@ -187,6 +191,55 @@ export function makeMobileApiRoutes(deps) {
187
191
  });
188
192
  }
189
193
  }
194
+ else if (method === MOBILE_COMMAND_METHOD) {
195
+ const payload = parsed.payload;
196
+ const sessionId = payload?.sessionId;
197
+ const line = payload?.line;
198
+ if (typeof sessionId !== 'string' || typeof line !== 'string') {
199
+ writeJson(res, 200, {
200
+ type: 'server-response',
201
+ rpcId,
202
+ result: { ok: false, error: { code: 'bad-request', message: 'missing sessionId or line' } },
203
+ });
204
+ }
205
+ else if (deps.commandDispatcher === undefined) {
206
+ writeJson(res, 200, {
207
+ type: 'server-response',
208
+ rpcId,
209
+ result: { ok: false, error: { code: 'unavailable', message: 'command dispatcher is not available' } },
210
+ });
211
+ }
212
+ else {
213
+ try {
214
+ const abort = new AbortController();
215
+ res.on('close', () => { if (!res.writableEnded)
216
+ abort.abort(); });
217
+ const outcome = await deps.commandDispatcher.execute(sessionId, line, abort.signal);
218
+ if ('error' in outcome) {
219
+ writeJson(res, 200, {
220
+ type: 'server-response',
221
+ rpcId,
222
+ result: { ok: false, error: { code: outcome.error, message: outcome.error } },
223
+ });
224
+ }
225
+ else {
226
+ writeJson(res, 200, {
227
+ type: 'server-response',
228
+ rpcId,
229
+ result: { ok: true, value: { matched: true, result: outcome.result } },
230
+ });
231
+ }
232
+ }
233
+ catch (error) {
234
+ const message = error instanceof Error ? error.message : String(error);
235
+ writeJson(res, 200, {
236
+ type: 'server-response',
237
+ rpcId,
238
+ result: { ok: false, error: { code: 'internal', message } },
239
+ });
240
+ }
241
+ }
242
+ }
190
243
  return;
191
244
  }
192
245
  try {
@@ -345,5 +398,7 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
345
398
  return wrap(await apiProxy.sessions.selectModel(request));
346
399
  if (method === 'session.rename')
347
400
  return wrap(await apiProxy.sessions.rename(request));
401
+ if (method === 'session.cancel')
402
+ return wrap(await apiProxy.sessions.cancel(request));
348
403
  throw new Error(`unhandled allowlisted method ${method}`);
349
404
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAwH/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CAyE7C"}
1
+ {"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAwH/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CA8F7C"}
@@ -7,6 +7,7 @@
7
7
  import { readFile } from 'node:fs/promises';
8
8
  import { existsSync } from 'node:fs';
9
9
  import { fileURLToPath } from 'node:url';
10
+ import { gzipSync } from 'node:zlib';
10
11
  const MOBILE_ROOT = '/m/';
11
12
  const MOBILE_BUNDLE_URL = '/m/mobile.js';
12
13
  const MOBILE_MANIFEST_URL = '/m/manifest.webmanifest';
@@ -132,7 +133,8 @@ export function makeMobileRoutes() {
132
133
  // The bundle is immutable for the process lifetime (a rebuild requires a
133
134
  // host restart), so the body is read from disk once, not per phone.
134
135
  let bundleBody;
135
- const handleBundle = async (_req, res) => {
136
+ let bundleGzip;
137
+ const handleBundle = async (req, res) => {
136
138
  if (bundleBody === undefined) {
137
139
  const path = mobileBundlePath();
138
140
  if (!existsSync(path)) {
@@ -141,13 +143,34 @@ export function makeMobileRoutes() {
141
143
  }
142
144
  try {
143
145
  bundleBody = await readFile(path, 'utf8');
146
+ bundleGzip = gzipSync(Buffer.from(bundleBody, 'utf8'));
144
147
  }
145
148
  catch {
146
149
  writeStatic(res, 500, 'text/plain', 'failed to read the mobile bundle');
147
150
  return;
148
151
  }
149
152
  }
150
- writeStatic(res, 200, 'text/javascript', bundleBody);
153
+ const acceptEncoding = req.headers['accept-encoding'] ?? '';
154
+ const acceptsGzip = typeof acceptEncoding === 'string' && acceptEncoding.includes('gzip') && bundleGzip !== undefined;
155
+ if (acceptsGzip) {
156
+ res.writeHead(200, {
157
+ 'content-type': 'text/javascript; charset=utf-8',
158
+ 'content-encoding': 'gzip',
159
+ 'cache-control': 'no-cache',
160
+ vary: 'Accept-Encoding',
161
+ 'referrer-policy': 'no-referrer',
162
+ });
163
+ res.end(bundleGzip);
164
+ }
165
+ else {
166
+ res.writeHead(200, {
167
+ 'content-type': 'text/javascript; charset=utf-8',
168
+ 'cache-control': 'no-cache',
169
+ vary: 'Accept-Encoding',
170
+ 'referrer-policy': 'no-referrer',
171
+ });
172
+ res.end(bundleBody);
173
+ }
151
174
  };
152
175
  let workerBody;
153
176
  const handleWorker = async (_req, res) => {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Host single-instance guard shared by the plugin family. The family bundle
3
- * (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
3
+ * (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
4
4
  * the loader accepts a standalone install of the same package side by side;
5
5
  * without this guard the second instance would still re-register the same
6
6
  * webserver routes, tools, settings namespaces, and system-prompt sections
@@ -1,7 +1,7 @@
1
1
  // Generated by scripts/sync-shared.mjs from shared/host/mount-once.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
2
  /**
3
3
  * Host single-instance guard shared by the plugin family. The family bundle
4
- * (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
4
+ * (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
5
5
  * the loader accepts a standalone install of the same package side by side;
6
6
  * without this guard the second instance would still re-register the same
7
7
  * webserver routes, tools, settings namespaces, and system-prompt sections
@@ -14,7 +14,7 @@
14
14
  * `ctx.effect` runs its callback immediately and treats the callback's
15
15
  * return value as the fiber disposer, so the unmarker is returned, not run.
16
16
  */
17
- const MOUNTED = Symbol.for('dsh-web-ui.mounted-plugins');
17
+ const MOUNTED = Symbol.for('dsh-web.mounted-plugins');
18
18
  function mountedSet() {
19
19
  const registry = globalThis;
20
20
  return (registry[MOUNTED] ??= new Set());
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Remote update support for the dsh-web-ui family — host half. Detects the
3
- * installed aggregate package (@linxin666/dsh-web-ui-all), or the directly
2
+ * Remote update support for the dsh-web family — host half. Detects the
3
+ * installed aggregate package (@linxin666/dsh-web-all), or the directly
4
4
  * installed family packages when the aggregate is absent, probes npm for newer
5
5
  * releases, and runs `pnpm update --latest` inside the owning dsh profile.
6
6
  *
@@ -11,12 +11,16 @@
11
11
  import { spawn } from 'node:child_process';
12
12
  /** npm registry base used for version probes. */
13
13
  export declare const NPM_REGISTRY = "https://registry.npmjs.org";
14
- /** The family scope every dsh-web-ui package is published under. */
14
+ /** The family scope every dsh-web package is published under. */
15
15
  export declare const FAMILY_SCOPE = "@linxin666/";
16
16
  /** The aggregate package that is the canonical update entry point. */
17
- export declare const AGGREGATE_PACKAGE = "@linxin666/dsh-web-ui-all";
17
+ export declare const AGGREGATE_PACKAGE = "@linxin666/dsh-web-all";
18
18
  /** Fallback anchor: this plugin's own package when the aggregate is absent. */
19
19
  export declare const SELF_PACKAGE = "@linxin666/dsh-remote-web-ui";
20
+ /** GitHub repository used to surface human-readable release notes. */
21
+ export declare const UPDATE_RELEASE_REPO = "zhu1090093659/dsh-web";
22
+ /** Release-notes cache freshness window (host process). */
23
+ export declare const RELEASE_NOTES_CACHE_TTL_MS: number;
20
24
  /** A parsed semantic version (prerelease identifiers kept as strings). */
21
25
  export interface SemverParts {
22
26
  major: number;
@@ -74,6 +78,17 @@ export interface UpdatePackageStatus {
74
78
  /** Whether npm carries a strictly newer release. */
75
79
  outdated: boolean;
76
80
  }
81
+ /** Structured release-note sections shown in the update panel. */
82
+ export interface UpdateReleaseNotes {
83
+ /** Version the notes describe. */
84
+ version: string;
85
+ /** New feature bullets. */
86
+ features: string[];
87
+ /** Bug-fix bullets. */
88
+ fixes: string[];
89
+ /** Other-change bullets. */
90
+ other: string[];
91
+ }
77
92
  /** The full update-status snapshot served to the browser half. */
78
93
  export interface UpdateStatus {
79
94
  /** npm = registry-managed (updatable); link = local dev install; missing = no anchor package. */
@@ -86,6 +101,8 @@ export interface UpdateStatus {
86
101
  packages: UpdatePackageStatus[];
87
102
  /** True when any package has a newer npm release. */
88
103
  outdated: boolean;
104
+ /** Human-readable release notes for the target release, when available. */
105
+ notes?: UpdateReleaseNotes;
89
106
  /** Whole-check failure (e.g. registry unreachable). */
90
107
  error?: string;
91
108
  }
@@ -97,6 +114,8 @@ export interface UpdateCheckDeps {
97
114
  resolve(specifier: string): string | undefined;
98
115
  /** Probe one package's latest npm version; undefined on failure. */
99
116
  fetchLatest(name: string): Promise<string | undefined>;
117
+ /** Fetch structured release notes for one target version, when available. */
118
+ fetchReleaseNotes?(version: string): Promise<UpdateReleaseNotes | undefined>;
100
119
  }
101
120
  /**
102
121
  * Resolve the anchor package's manifest path. The aggregate package is the
@@ -141,6 +160,18 @@ export declare function fetchLatestVersion(name: string, fetchImpl: (url: string
141
160
  ok: boolean;
142
161
  json(): Promise<unknown>;
143
162
  }>, timeoutMs?: number): Promise<string | undefined>;
163
+ export declare function parseReleaseNotesBody(version: string, body: string): UpdateReleaseNotes | undefined;
164
+ /**
165
+ * Fetch and parse the GitHub Release for one `dsh-web` version.
166
+ * @param version - the npm release version (without the `v` prefix).
167
+ * @param fetchImpl - injected fetch; global `fetch` in the host.
168
+ * @param timeoutMs - hard timeout for the GitHub API probe.
169
+ * @returns structured notes, or undefined when unavailable.
170
+ */
171
+ export declare function fetchGitHubReleaseNotes(version: string, fetchImpl?: (url: string, init?: RequestInit) => Promise<{
172
+ ok: boolean;
173
+ json(): Promise<unknown>;
174
+ }>, timeoutMs?: number): Promise<UpdateReleaseNotes | undefined>;
144
175
  /**
145
176
  * Build the update status: locate the anchor, detect the install mode, and
146
177
  * compare every family package against the npm registry.
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,iDAAiD;AACjD,eAAO,MAAM,YAAY,+BAA+B,CAAA;AAExD,oEAAoE;AACpE,eAAO,MAAM,YAAY,gBAAgB,CAAA;AAEzC,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,8BAA8B,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,YAAY,iCAAiC,CAAA;AAQ1D,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CASlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B5D;AAYD,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAahF;AAED,6DAA6D;AAC7D,KAAK,cAAc,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAE9D,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAG1D;AAcD,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,iGAAiG;IACjG,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;IAChC,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,QAAQ,EAAE,mBAAmB,EAAE,CAAA;IAC/B,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAA;IACjB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC9C,oEAAoE;IACpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACjD,MAAM,GAAG,SAAS,CAUpB;AAED,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAA;IAClB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,GACpC,YAAY,GAAG;IAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAAA;CAAE,CAoBhD;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAQhF;AAiBD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,EAClG,SAAS,SAAS,GACjB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiB7B;AA4BD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAuD/E;AAED,4DAA4D;AAC5D,MAAM,MAAM,eAAe;AACzB,2BAA2B;AACzB,cAAc;AAChB,6CAA6C;GAC3C,SAAS;AACX,2CAA2C;GACzC,WAAW;AACb,iEAAiE;GAC/D,MAAM;AACR,4BAA4B;GAC1B,aAAa;AACf,6DAA6D;GAC3D,OAAO;AACT,iFAAiF;GAC/E,eAAe,CAAA;AAEnB,gCAAgC;AAChC,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAA;IACX,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,SAAS,CAAC,EAAE,eAAe,CAAA;CAC5B;AAED,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAA;IAClB,sCAAsC;IACtC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAA;CAC3B;AAuBD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CA8HvE;AAED,0EAA0E;AAC1E,MAAM,WAAW,qBAAqB;IACpC,iDAAiD;IACjD,GAAG,EAAE,aAAa,CAAA;IAClB,8DAA8D;IAC9D,KAAK,EAAE,eAAe,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAgE7F"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Remote update support for the dsh-web-ui family — host half. Detects the
3
- * installed aggregate package (@linxin666/dsh-web-ui-all), or the directly
2
+ * Remote update support for the dsh-web family — host half. Detects the
3
+ * installed aggregate package (@linxin666/dsh-web-all), or the directly
4
4
  * installed family packages when the aggregate is absent, probes npm for newer
5
5
  * releases, and runs `pnpm update --latest` inside the owning dsh profile.
6
6
  *
@@ -13,12 +13,16 @@ import { dirname, join } from 'node:path';
13
13
  import { spawn } from 'node:child_process';
14
14
  /** npm registry base used for version probes. */
15
15
  export const NPM_REGISTRY = 'https://registry.npmjs.org';
16
- /** The family scope every dsh-web-ui package is published under. */
16
+ /** The family scope every dsh-web package is published under. */
17
17
  export const FAMILY_SCOPE = '@linxin666/';
18
18
  /** The aggregate package that is the canonical update entry point. */
19
- export const AGGREGATE_PACKAGE = '@linxin666/dsh-web-ui-all';
19
+ export const AGGREGATE_PACKAGE = '@linxin666/dsh-web-all';
20
20
  /** Fallback anchor: this plugin's own package when the aggregate is absent. */
21
21
  export const SELF_PACKAGE = '@linxin666/dsh-remote-web-ui';
22
+ /** GitHub repository used to surface human-readable release notes. */
23
+ export const UPDATE_RELEASE_REPO = 'zhu1090093659/dsh-web';
24
+ /** Release-notes cache freshness window (host process). */
25
+ export const RELEASE_NOTES_CACHE_TTL_MS = 10 * 60_000;
22
26
  /** A profile manifest `name` prefix (e.g. `dsh-profile-web`). */
23
27
  const PROFILE_NAME_PREFIX = 'dsh-profile-';
24
28
  /** How many ancestor directories a profile search walks before giving up. */
@@ -242,6 +246,91 @@ export async function fetchLatestVersion(name, fetchImpl, timeoutMs = 10_000) {
242
246
  return undefined;
243
247
  }
244
248
  }
249
+ /** Normalize one release-note bullet. */
250
+ function cleanNoteItem(text) {
251
+ // Keep the visible label and drop Markdown link targets; issue references
252
+ // become plain #123 because the panel has no Markdown renderer.
253
+ return text.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/^[-*]\s+/, '').trim();
254
+ }
255
+ /** Determine the section kind for a Markdown heading line. */
256
+ function noteSectionOf(line) {
257
+ const heading = /^#{1,6}\s+(.+)$/.exec(line.trim());
258
+ if (heading === null)
259
+ return undefined;
260
+ const title = heading[1].replace(/[::].*$/, '').trim().toLowerCase();
261
+ if (title === '新功能' || title === 'new features')
262
+ return 'features';
263
+ if (title === '修复' || title === 'bug fixes' || title === 'bugfixes')
264
+ return 'fixes';
265
+ if (title === '其他改动' || title === 'other changes')
266
+ return 'other';
267
+ return undefined;
268
+ }
269
+ /**
270
+ * Parse the GitHub Release body into the three conventional sections.
271
+ *
272
+ * The committed release notes are bilingual: Chinese is the default view and
273
+ * English lives inside a `<details>` block. Prefer the Chinese sections when
274
+ * present and fall back to the whole body for English-only releases. Bullets
275
+ * are escaped as plain text for the panel (no Markdown renderer).
276
+ */
277
+ function parseReleaseNotesSource(version, source) {
278
+ const notes = { version, features: [], fixes: [], other: [] };
279
+ let current;
280
+ for (const rawLine of source.split(/\r?\n/)) {
281
+ const line = rawLine.trim();
282
+ const section = noteSectionOf(line);
283
+ if (section !== undefined) {
284
+ current = section;
285
+ continue;
286
+ }
287
+ if (current === undefined || !/^-\s+/.test(line))
288
+ continue;
289
+ const item = cleanNoteItem(line);
290
+ if (item !== '')
291
+ notes[current].push(item);
292
+ }
293
+ if (notes.features.length === 0 && notes.fixes.length === 0 && notes.other.length === 0)
294
+ return undefined;
295
+ return notes;
296
+ }
297
+ export function parseReleaseNotesBody(version, body) {
298
+ // The committed release notes put Chinese in the default view and English in
299
+ // a <details> block. Parse only the default view first, then fall back to
300
+ // the full body for English-only releases.
301
+ return parseReleaseNotesSource(version, body.split('<details>')[0] ?? body)
302
+ ?? parseReleaseNotesSource(version, body);
303
+ }
304
+ /**
305
+ * Fetch and parse the GitHub Release for one `dsh-web` version.
306
+ * @param version - the npm release version (without the `v` prefix).
307
+ * @param fetchImpl - injected fetch; global `fetch` in the host.
308
+ * @param timeoutMs - hard timeout for the GitHub API probe.
309
+ * @returns structured notes, or undefined when unavailable.
310
+ */
311
+ export async function fetchGitHubReleaseNotes(version, fetchImpl = fetch, timeoutMs = 10_000) {
312
+ const controller = new AbortController();
313
+ const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
314
+ try {
315
+ const response = await fetchImpl(`https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`, {
316
+ headers: { accept: 'application/vnd.github+json', 'user-agent': 'dsh-web-update' },
317
+ signal: controller.signal,
318
+ });
319
+ if (!response.ok)
320
+ return undefined;
321
+ const body = await response.json();
322
+ if (typeof body !== 'object' || body === null)
323
+ return undefined;
324
+ const text = body.body;
325
+ return typeof text === 'string' ? parseReleaseNotesBody(version, text) : undefined;
326
+ }
327
+ catch {
328
+ return undefined;
329
+ }
330
+ finally {
331
+ clearTimeout(timer);
332
+ }
333
+ }
245
334
  /**
246
335
  * Sentinel for an installed version that could not be read (missing manifest
247
336
  * or resolve failure). It is a real-looking version so `checkUpdates` can
@@ -319,12 +408,17 @@ export async function checkUpdates(deps) {
319
408
  // Registry unreachable: every probe failed — report the outage distinctly
320
409
  // instead of a misleading "all up to date" (the panel needs the reason).
321
410
  const error = probeFailures === names.length && names.length > 0 ? 'registry-unreachable' : undefined;
411
+ const targetVersion = packages.find(packageStatus => packageStatus.latest !== undefined)?.latest;
412
+ const notes = targetVersion !== undefined && deps.fetchReleaseNotes !== undefined
413
+ ? await deps.fetchReleaseNotes(targetVersion).catch(() => undefined)
414
+ : undefined;
322
415
  return {
323
416
  mode: linked ? 'link' : 'npm',
324
417
  profileName: profile.name,
325
418
  anchor,
326
419
  packages,
327
420
  outdated: packages.some(packageStatus => packageStatus.outdated),
421
+ ...(notes !== undefined ? { notes } : {}),
328
422
  ...(error !== undefined ? { error } : {}),
329
423
  };
330
424
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-remote-web-ui",
3
3
  "description": "Scan-to-pair mobile and PC remote control for the dsh web GUI: QR entry beside the settings button, one-time pairing tokens, a gated /remote/api desktop channel, live device status, and revocable sessions",
4
- "version": "0.3.2",
4
+ "version": "0.3.4",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -75,6 +75,8 @@
75
75
  "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
76
76
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
77
77
  "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
78
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
79
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
78
80
  "@testing-library/dom": "^10.4.1",
79
81
  "@testing-library/react": "^16.3.2",
80
82
  "@types/node": "^22.20.0",
@@ -90,7 +92,7 @@
90
92
  },
91
93
  "repository": {
92
94
  "type": "git",
93
- "url": "https://github.com/zhu1090093659/dsh-web-ui.git"
95
+ "url": "https://github.com/zhu1090093659/dsh-web.git"
94
96
  },
95
97
  "scripts": {
96
98
  "build": "tsc -b && tsdown",
@@ -39,6 +39,8 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
39
39
  titleKey: TKey
40
40
  /** Locale key of the line describing what this plugin's settings govern. */
41
41
  descriptionKey: TKey
42
+ /** Optional rich header description rendered instead of the plain t(descriptionKey) text (the plain text stays as the tooltip). */
43
+ descriptionNode?: ReactNode
42
44
  /** The card's form state: availability, writability, and what a save would do. */
43
45
  state: CardShell
44
46
  /** Write every staged edit. */
@@ -87,7 +89,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
87
89
  <div className={css.headerStatic}>
88
90
  <span className={css.headText}>
89
91
  <span className={css.name} title={title}>{title}</span>
90
- <span className={css.description} title={description}>{description}</span>
92
+ <span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
91
93
  </span>
92
94
  {state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
93
95
  </div>
@@ -102,7 +104,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
102
104
  >
103
105
  <span className={css.headText}>
104
106
  <span className={css.name} title={title}>{title}</span>
105
- <span className={css.description} title={description}>{description}</span>
107
+ <span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
106
108
  </span>
107
109
  {state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
108
110
  <svg
@@ -3,7 +3,7 @@
3
3
  * trigger beside the settings button, and the pairing panel modal. Owns the
4
4
  * panel behavior — token minting on open, the status SSE subscription,
5
5
  * stop/refresh/copy — and renders the pure {@link RemotePanel} body. The
6
- * update seat (the dsh-web-ui self-update flow) rides the same footer row,
6
+ * update seat (the dsh-web self-update flow) rides the same footer row,
7
7
  * rendered by {@link UpdateEntry}. Component-local state per the client
8
8
  * stack rules: nothing here survives remounts or crosses entries.
9
9
  */
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
8
8
  import { IconCloseOutline16, IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
9
- import type { UpdateRunResult, UpdateStatus } from '../update.ts'
9
+ import type { UpdateReleaseNotes, UpdateRunResult, UpdateStatus } from '../update.ts'
10
10
  import css from "./remote.module.css"
11
11
 
12
12
  /** The panel view state, owned by the entry component. */
@@ -73,7 +73,7 @@ export function UpdatePanel({ t, view, onClose, onRecheck, onStartUpdate }: Upda
73
73
  {t("update.updating", { name: anchorName(status) ?? "", version: anchorLatest(status) ?? "" })}
74
74
  </p>
75
75
  <p className={css.updateDetail}>{t("update.cooldownNotice")}</p>
76
- <PackageList status={status} />
76
+ <PackageSummary t={t} status={status} />
77
77
  </div>
78
78
  )}
79
79
  {view.kind === "done" && <DoneBody t={t} result={view.result} />}
@@ -147,6 +147,7 @@ function ResultBody({ t, status }: { t: TranslateNS<"remote">; status: UpdateSta
147
147
  {anchor !== undefined ? t("update.foundDetail", { name: anchor, version: latest ?? "" }) : ""}
148
148
  </p>
149
149
  <p className={css.updateDetail}>{t("update.cooldownNotice")}</p>
150
+ <PackageSummary t={t} status={status} />
150
151
  </div>
151
152
  )
152
153
  }
@@ -156,7 +157,46 @@ function ResultBody({ t, status }: { t: TranslateNS<"remote">; status: UpdateSta
156
157
  {anchor !== undefined && latest !== undefined && (
157
158
  <p className={css.updateDetail}>{t("update.upToDateDetail", { name: anchor, version: latest })}</p>
158
159
  )}
159
- <PackageList status={status} />
160
+ <PackageSummary t={t} status={status} />
161
+ </div>
162
+ )
163
+ }
164
+
165
+ /** Release-note summary when available; otherwise fall back to the package list. */
166
+ function PackageSummary({ t, status }: { t: TranslateNS<'remote'>; status: UpdateStatus }) {
167
+ if (status.notes === undefined) return <PackageList status={status} />
168
+ return (
169
+ <div>
170
+ <ReleaseNotes t={t} notes={status.notes} />
171
+ <details className={css.updateVersions}>
172
+ <summary className={css.updateVersionsSummary}>{t("update.componentVersions")}</summary>
173
+ <PackageList status={status} />
174
+ </details>
175
+ </div>
176
+ )
177
+ }
178
+
179
+ /** Render GitHub Release sections as a compact three-group list. */
180
+ function ReleaseNotes({ t, notes }: { t: TranslateNS<'remote'>; notes: UpdateReleaseNotes }) {
181
+ const sections = [
182
+ { key: 'features', title: t("update.releaseFeatures"), items: notes.features },
183
+ { key: 'fixes', title: t("update.releaseFixes"), items: notes.fixes },
184
+ { key: 'other', title: t("update.releaseOther"), items: notes.other },
185
+ ].filter(section => section.items.length > 0)
186
+ if (sections.length === 0) return <p className={css.updateDetail}>{t("update.releaseUnavailable")}</p>
187
+ return (
188
+ <div className={css.updateNotes}>
189
+ <h3 className={css.updateNotesTitle}>{t("update.releaseNotes", { version: notes.version })}</h3>
190
+ {sections.map(section => (
191
+ <section key={section.key} className={css.updateNoteSection}>
192
+ <h4 className={css.updateNoteHeading}>{section.title}</h4>
193
+ <ul className={css.updateNoteList}>
194
+ {section.items.map((item, index) => (
195
+ <li key={index} className={css.updateNoteItem}>{item}</li>
196
+ ))}
197
+ </ul>
198
+ </section>
199
+ ))}
160
200
  </div>
161
201
  )
162
202
  }
@@ -33,6 +33,7 @@ import {
33
33
  type RemoteChannelBootSeat,
34
34
  } from './remote-channel.ts'
35
35
  import { FenceNotice } from './FenceNotice.tsx'
36
+ import { reportDailyHeartbeat } from './telemetry.ts'
36
37
 
37
38
  export type { RemoteEntryProps } from './RemoteEntry.tsx'
38
39
  export type { PanelState, RemotePanelProps } from './RemotePanel.tsx'
@@ -80,7 +81,7 @@ export interface SettingsPluginItemOwnerProps {
80
81
  declare module '@deepseek-ai/cordis' {
81
82
  interface Context {
82
83
  /**
83
- * Optional rc.6 compatibility binder provided by dsh-web-ui-settings;
84
+ * Optional rc.6 compatibility binder provided by dsh-web-settings;
84
85
  * absent when that group plugin is not installed, so callers fall back to
85
86
  * the official settings scope.
86
87
  */
@@ -106,6 +107,10 @@ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote
106
107
  * @param ctx - client root context.
107
108
  */
108
109
  export function apply(ctx: ClientContext): void {
110
+ // Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
111
+ // UTC day, package name only, silent failure.
112
+ reportDailyHeartbeat([{ name: '@linxin666/dsh-remote-web-ui' }])
113
+
109
114
  ctx.effect(() => {
110
115
  try {
111
116
  return ctx.locale.register(NS, { zh, en })