@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.
- package/README.i18n.yaml +2 -2
- package/README.md +22 -8
- package/README.zh.md +12 -7
- package/assets/mobile-service-worker.js +1 -2
- package/lib/client.js +177 -19
- package/lib/client.js.map +1 -1
- package/lib/index.js +222 -15
- package/lib/mobile.js +64 -13
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +2 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/RemoteEntry.js +1 -1
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +20 -3
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +4 -0
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +22 -10
- package/lib/types/client/settings-form.d.ts +1 -1
- package/lib/types/client/settings-form.js +1 -1
- package/lib/types/client/telemetry.d.ts +27 -0
- package/lib/types/client/telemetry.d.ts.map +1 -0
- package/lib/types/client/telemetry.js +97 -0
- package/lib/types/dsh-home.d.ts.map +1 -1
- package/lib/types/dsh-home.js +9 -3
- package/lib/types/http.d.ts +1 -1
- package/lib/types/http.js +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +34 -5
- package/lib/types/loopback.d.ts +1 -1
- package/lib/types/loopback.js +1 -1
- package/lib/types/mobile/api.d.ts +7 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +9 -3
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +10 -0
- package/lib/types/mobile/views/ChatView.d.ts +1 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +32 -5
- package/lib/types/mobile/views/ChatView.stream.test.js +28 -2
- package/lib/types/mobile/views/ChatView.test.js +152 -2
- package/lib/types/mobile-api.d.ts +14 -0
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +56 -1
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +25 -2
- package/lib/types/mount-once.d.ts +1 -1
- package/lib/types/mount-once.js +2 -2
- package/lib/types/update.d.ts +35 -4
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +98 -4
- package/package.json +4 -2
- package/src/client/PluginSettingsCard.tsx +4 -2
- package/src/client/RemoteEntry.tsx +1 -1
- package/src/client/UpdatePanel.tsx +43 -3
- package/src/client/index.ts +6 -1
- package/src/client/locales.ts +22 -10
- package/src/client/remote.module.css +56 -3
- package/src/client/settings-card.module.css +5 -0
- package/src/client/settings-form.ts +1 -1
- package/src/client/telemetry.ts +105 -0
- package/src/dsh-home.ts +9 -3
- package/src/http.ts +1 -1
- package/src/index.ts +36 -4
- package/src/loopback.ts +1 -1
- package/src/mobile/api.ts +10 -3
- package/src/mobile/mobile-styles.ts +10 -0
- package/src/mobile/views/ChatView.stream.test.tsx +34 -2
- package/src/mobile/views/ChatView.test.tsx +177 -2
- package/src/mobile/views/ChatView.tsx +44 -6
- package/src/mobile-api.ts +61 -0
- package/src/mobile-routes.ts +24 -2
- package/src/mount-once.ts +2 -2
- package/src/update.ts +117 -4
package/lib/index.js
CHANGED
|
@@ -6,9 +6,11 @@ import z from "schemastery";
|
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { homedir, networkInterfaces } from "node:os";
|
|
9
|
+
import { isAbsolute as isAbsolute$1, join as join$1 } from "node:path/posix";
|
|
9
10
|
import { z as z$1 } from "zod";
|
|
10
11
|
import { readFile } from "node:fs/promises";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { gzipSync } from "node:zlib";
|
|
12
14
|
import { RpcId } from "@deepseek-ai/dsh-host-apiproxy/api/rpc";
|
|
13
15
|
import http, { request } from "node:http";
|
|
14
16
|
import { connect } from "node:net";
|
|
@@ -650,8 +652,9 @@ function sanitizeUserAgent(raw) {
|
|
|
650
652
|
*/
|
|
651
653
|
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
652
654
|
function expandHome(path, home = homedir()) {
|
|
655
|
+
const j = home.startsWith("/") ? join$1 : join;
|
|
653
656
|
if (path === "~") return home;
|
|
654
|
-
if (path.startsWith("~/") || path.startsWith("~\\")) return
|
|
657
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return j(home, path.slice(2));
|
|
655
658
|
return path;
|
|
656
659
|
}
|
|
657
660
|
/**
|
|
@@ -661,12 +664,15 @@ function expandHome(path, home = homedir()) {
|
|
|
661
664
|
* @returns the absolute DSH home path.
|
|
662
665
|
*/
|
|
663
666
|
function resolveDshHome(env = process.env, home = homedir()) {
|
|
667
|
+
const isPosix = home.startsWith("/");
|
|
668
|
+
const j = isPosix ? join$1 : join;
|
|
669
|
+
const isAbs = isPosix ? isAbsolute$1 : isAbsolute;
|
|
664
670
|
const raw = env.DSH_HOME;
|
|
665
671
|
if (raw !== void 0 && raw.trim() !== "") {
|
|
666
672
|
const expanded = expandHome(raw.trim(), home);
|
|
667
|
-
return
|
|
673
|
+
return isAbs(expanded) ? expanded : j(process.cwd(), expanded);
|
|
668
674
|
}
|
|
669
|
-
return
|
|
675
|
+
return j(home, ".dsh");
|
|
670
676
|
}
|
|
671
677
|
/** Resolve the DSH home directory from the live environment. */
|
|
672
678
|
function dshHome() {
|
|
@@ -1442,7 +1448,8 @@ function makeMobileRoutes() {
|
|
|
1442
1448
|
writeStatic(res, 200, "text/html", offlineHtml());
|
|
1443
1449
|
};
|
|
1444
1450
|
let bundleBody;
|
|
1445
|
-
|
|
1451
|
+
let bundleGzip;
|
|
1452
|
+
const handleBundle = async (req, res) => {
|
|
1446
1453
|
if (bundleBody === void 0) {
|
|
1447
1454
|
const path = mobileBundlePath();
|
|
1448
1455
|
if (!existsSync(path)) {
|
|
@@ -1451,12 +1458,31 @@ function makeMobileRoutes() {
|
|
|
1451
1458
|
}
|
|
1452
1459
|
try {
|
|
1453
1460
|
bundleBody = await readFile(path, "utf8");
|
|
1461
|
+
bundleGzip = gzipSync(Buffer.from(bundleBody, "utf8"));
|
|
1454
1462
|
} catch {
|
|
1455
1463
|
writeStatic(res, 500, "text/plain", "failed to read the mobile bundle");
|
|
1456
1464
|
return;
|
|
1457
1465
|
}
|
|
1458
1466
|
}
|
|
1459
|
-
|
|
1467
|
+
const acceptEncoding = req.headers["accept-encoding"] ?? "";
|
|
1468
|
+
if (typeof acceptEncoding === "string" && acceptEncoding.includes("gzip") && bundleGzip !== void 0) {
|
|
1469
|
+
res.writeHead(200, {
|
|
1470
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1471
|
+
"content-encoding": "gzip",
|
|
1472
|
+
"cache-control": "no-cache",
|
|
1473
|
+
vary: "Accept-Encoding",
|
|
1474
|
+
"referrer-policy": "no-referrer"
|
|
1475
|
+
});
|
|
1476
|
+
res.end(bundleGzip);
|
|
1477
|
+
} else {
|
|
1478
|
+
res.writeHead(200, {
|
|
1479
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1480
|
+
"cache-control": "no-cache",
|
|
1481
|
+
vary: "Accept-Encoding",
|
|
1482
|
+
"referrer-policy": "no-referrer"
|
|
1483
|
+
});
|
|
1484
|
+
res.end(bundleBody);
|
|
1485
|
+
}
|
|
1460
1486
|
};
|
|
1461
1487
|
let workerBody;
|
|
1462
1488
|
const handleWorker = async (_req, res) => {
|
|
@@ -1557,7 +1583,8 @@ const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
|
1557
1583
|
"session.prompt",
|
|
1558
1584
|
"session.models",
|
|
1559
1585
|
"session.selectModel",
|
|
1560
|
-
"session.rename"
|
|
1586
|
+
"session.rename",
|
|
1587
|
+
"session.cancel"
|
|
1561
1588
|
]);
|
|
1562
1589
|
/**
|
|
1563
1590
|
* Locally answered display-preference method (the phone's read-only
|
|
@@ -1567,6 +1594,8 @@ const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
|
1567
1594
|
const MOBILE_PREFERENCES_METHOD = "mobile.preferences";
|
|
1568
1595
|
const MOBILE_PENDING_METHOD = "mobile.pending";
|
|
1569
1596
|
const MOBILE_RESPOND_METHOD = "mobile.respond";
|
|
1597
|
+
/** Execute a slash command through the host command registry (#1125). */
|
|
1598
|
+
const MOBILE_COMMAND_METHOD = "mobile.command";
|
|
1570
1599
|
/** One session.list page (thin phones load incrementally). */
|
|
1571
1600
|
const SESSION_PAGE_SIZE = 20;
|
|
1572
1601
|
/** SSE keep-alive ping cadence for the live mux stream (single connection). */
|
|
@@ -1649,7 +1678,7 @@ function makeMobileApiRoutes(deps) {
|
|
|
1649
1678
|
return;
|
|
1650
1679
|
}
|
|
1651
1680
|
const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length);
|
|
1652
|
-
const local = method === MOBILE_PREFERENCES_METHOD || method === MOBILE_PENDING_METHOD || method === MOBILE_RESPOND_METHOD;
|
|
1681
|
+
const local = method === MOBILE_PREFERENCES_METHOD || method === MOBILE_PENDING_METHOD || method === MOBILE_RESPOND_METHOD || method === MOBILE_COMMAND_METHOD;
|
|
1653
1682
|
if (!MOBILE_ALLOWLIST.has(method) && !local) {
|
|
1654
1683
|
writeJson(res, 403, {
|
|
1655
1684
|
ok: false,
|
|
@@ -1735,6 +1764,73 @@ function makeMobileApiRoutes(deps) {
|
|
|
1735
1764
|
}
|
|
1736
1765
|
});
|
|
1737
1766
|
}
|
|
1767
|
+
} else if (method === MOBILE_COMMAND_METHOD) {
|
|
1768
|
+
const payload = parsed.payload;
|
|
1769
|
+
const sessionId = payload?.sessionId;
|
|
1770
|
+
const line = payload?.line;
|
|
1771
|
+
if (typeof sessionId !== "string" || typeof line !== "string") writeJson(res, 200, {
|
|
1772
|
+
type: "server-response",
|
|
1773
|
+
rpcId,
|
|
1774
|
+
result: {
|
|
1775
|
+
ok: false,
|
|
1776
|
+
error: {
|
|
1777
|
+
code: "bad-request",
|
|
1778
|
+
message: "missing sessionId or line"
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
else if (deps.commandDispatcher === void 0) writeJson(res, 200, {
|
|
1783
|
+
type: "server-response",
|
|
1784
|
+
rpcId,
|
|
1785
|
+
result: {
|
|
1786
|
+
ok: false,
|
|
1787
|
+
error: {
|
|
1788
|
+
code: "unavailable",
|
|
1789
|
+
message: "command dispatcher is not available"
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
else try {
|
|
1794
|
+
const abort = new AbortController();
|
|
1795
|
+
res.on("close", () => {
|
|
1796
|
+
if (!res.writableEnded) abort.abort();
|
|
1797
|
+
});
|
|
1798
|
+
const outcome = await deps.commandDispatcher.execute(sessionId, line, abort.signal);
|
|
1799
|
+
if ("error" in outcome) writeJson(res, 200, {
|
|
1800
|
+
type: "server-response",
|
|
1801
|
+
rpcId,
|
|
1802
|
+
result: {
|
|
1803
|
+
ok: false,
|
|
1804
|
+
error: {
|
|
1805
|
+
code: outcome.error,
|
|
1806
|
+
message: outcome.error
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
else writeJson(res, 200, {
|
|
1811
|
+
type: "server-response",
|
|
1812
|
+
rpcId,
|
|
1813
|
+
result: {
|
|
1814
|
+
ok: true,
|
|
1815
|
+
value: {
|
|
1816
|
+
matched: true,
|
|
1817
|
+
result: outcome.result
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
writeJson(res, 200, {
|
|
1823
|
+
type: "server-response",
|
|
1824
|
+
rpcId,
|
|
1825
|
+
result: {
|
|
1826
|
+
ok: false,
|
|
1827
|
+
error: {
|
|
1828
|
+
code: "internal",
|
|
1829
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1738
1834
|
}
|
|
1739
1835
|
return;
|
|
1740
1836
|
}
|
|
@@ -1874,6 +1970,7 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
|
1874
1970
|
if (method === "session.models") return wrap(await apiProxy.sessions.models(request));
|
|
1875
1971
|
if (method === "session.selectModel") return wrap(await apiProxy.sessions.selectModel(request));
|
|
1876
1972
|
if (method === "session.rename") return wrap(await apiProxy.sessions.rename(request));
|
|
1973
|
+
if (method === "session.cancel") return wrap(await apiProxy.sessions.cancel(request));
|
|
1877
1974
|
throw new Error(`unhandled allowlisted method ${method}`);
|
|
1878
1975
|
}
|
|
1879
1976
|
//#endregion
|
|
@@ -2952,9 +3049,11 @@ var TunnelManager = class {
|
|
|
2952
3049
|
}
|
|
2953
3050
|
};
|
|
2954
3051
|
/** The aggregate package that is the canonical update entry point. */
|
|
2955
|
-
const AGGREGATE_PACKAGE = "@linxin666/dsh-web-
|
|
3052
|
+
const AGGREGATE_PACKAGE = "@linxin666/dsh-web-all";
|
|
2956
3053
|
/** Fallback anchor: this plugin's own package when the aggregate is absent. */
|
|
2957
3054
|
const SELF_PACKAGE = "@linxin666/dsh-remote-web-ui";
|
|
3055
|
+
/** GitHub repository used to surface human-readable release notes. */
|
|
3056
|
+
const UPDATE_RELEASE_REPO = "zhu1090093659/dsh-web";
|
|
2958
3057
|
/** A profile manifest `name` prefix (e.g. `dsh-profile-web`). */
|
|
2959
3058
|
const PROFILE_NAME_PREFIX = "dsh-profile-";
|
|
2960
3059
|
/** How many ancestor directories a profile search walks before giving up. */
|
|
@@ -3135,6 +3234,83 @@ async function fetchLatestVersion(name, fetchImpl, timeoutMs = 1e4) {
|
|
|
3135
3234
|
return;
|
|
3136
3235
|
}
|
|
3137
3236
|
}
|
|
3237
|
+
/** Normalize one release-note bullet. */
|
|
3238
|
+
function cleanNoteItem(text) {
|
|
3239
|
+
return text.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/^[-*]\s+/, "").trim();
|
|
3240
|
+
}
|
|
3241
|
+
/** Determine the section kind for a Markdown heading line. */
|
|
3242
|
+
function noteSectionOf(line) {
|
|
3243
|
+
const heading = /^#{1,6}\s+(.+)$/.exec(line.trim());
|
|
3244
|
+
if (heading === null) return void 0;
|
|
3245
|
+
const title = heading[1].replace(/[::].*$/, "").trim().toLowerCase();
|
|
3246
|
+
if (title === "新功能" || title === "new features") return "features";
|
|
3247
|
+
if (title === "修复" || title === "bug fixes" || title === "bugfixes") return "fixes";
|
|
3248
|
+
if (title === "其他改动" || title === "other changes") return "other";
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* Parse the GitHub Release body into the three conventional sections.
|
|
3252
|
+
*
|
|
3253
|
+
* The committed release notes are bilingual: Chinese is the default view and
|
|
3254
|
+
* English lives inside a `<details>` block. Prefer the Chinese sections when
|
|
3255
|
+
* present and fall back to the whole body for English-only releases. Bullets
|
|
3256
|
+
* are escaped as plain text for the panel (no Markdown renderer).
|
|
3257
|
+
*/
|
|
3258
|
+
function parseReleaseNotesSource(version, source) {
|
|
3259
|
+
const notes = {
|
|
3260
|
+
version,
|
|
3261
|
+
features: [],
|
|
3262
|
+
fixes: [],
|
|
3263
|
+
other: []
|
|
3264
|
+
};
|
|
3265
|
+
let current;
|
|
3266
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
3267
|
+
const line = rawLine.trim();
|
|
3268
|
+
const section = noteSectionOf(line);
|
|
3269
|
+
if (section !== void 0) {
|
|
3270
|
+
current = section;
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
if (current === void 0 || !/^-\s+/.test(line)) continue;
|
|
3274
|
+
const item = cleanNoteItem(line);
|
|
3275
|
+
if (item !== "") notes[current].push(item);
|
|
3276
|
+
}
|
|
3277
|
+
if (notes.features.length === 0 && notes.fixes.length === 0 && notes.other.length === 0) return void 0;
|
|
3278
|
+
return notes;
|
|
3279
|
+
}
|
|
3280
|
+
function parseReleaseNotesBody(version, body) {
|
|
3281
|
+
return parseReleaseNotesSource(version, body.split("<details>")[0] ?? body) ?? parseReleaseNotesSource(version, body);
|
|
3282
|
+
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Fetch and parse the GitHub Release for one `dsh-web` version.
|
|
3285
|
+
* @param version - the npm release version (without the `v` prefix).
|
|
3286
|
+
* @param fetchImpl - injected fetch; global `fetch` in the host.
|
|
3287
|
+
* @param timeoutMs - hard timeout for the GitHub API probe.
|
|
3288
|
+
* @returns structured notes, or undefined when unavailable.
|
|
3289
|
+
*/
|
|
3290
|
+
async function fetchGitHubReleaseNotes(version, fetchImpl = fetch, timeoutMs = 1e4) {
|
|
3291
|
+
const controller = new AbortController();
|
|
3292
|
+
const timer = setTimeout(() => {
|
|
3293
|
+
controller.abort();
|
|
3294
|
+
}, timeoutMs);
|
|
3295
|
+
try {
|
|
3296
|
+
const response = await fetchImpl(`https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`, {
|
|
3297
|
+
headers: {
|
|
3298
|
+
accept: "application/vnd.github+json",
|
|
3299
|
+
"user-agent": "dsh-web-update"
|
|
3300
|
+
},
|
|
3301
|
+
signal: controller.signal
|
|
3302
|
+
});
|
|
3303
|
+
if (!response.ok) return void 0;
|
|
3304
|
+
const body = await response.json();
|
|
3305
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
3306
|
+
const text = body.body;
|
|
3307
|
+
return typeof text === "string" ? parseReleaseNotesBody(version, text) : void 0;
|
|
3308
|
+
} catch {
|
|
3309
|
+
return;
|
|
3310
|
+
} finally {
|
|
3311
|
+
clearTimeout(timer);
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3138
3314
|
/**
|
|
3139
3315
|
* Sentinel for an installed version that could not be read (missing manifest
|
|
3140
3316
|
* or resolve failure). It is a real-looking version so `checkUpdates` can
|
|
@@ -3202,12 +3378,15 @@ async function checkUpdates(deps) {
|
|
|
3202
3378
|
});
|
|
3203
3379
|
});
|
|
3204
3380
|
const error = probeFailures === names.length && names.length > 0 ? "registry-unreachable" : void 0;
|
|
3381
|
+
const targetVersion = packages.find((packageStatus) => packageStatus.latest !== void 0)?.latest;
|
|
3382
|
+
const notes = targetVersion !== void 0 && deps.fetchReleaseNotes !== void 0 ? await deps.fetchReleaseNotes(targetVersion).catch(() => void 0) : void 0;
|
|
3205
3383
|
return {
|
|
3206
3384
|
mode: linked ? "link" : "npm",
|
|
3207
3385
|
profileName: profile.name,
|
|
3208
3386
|
anchor,
|
|
3209
3387
|
packages,
|
|
3210
3388
|
outdated: packages.some((packageStatus) => packageStatus.outdated),
|
|
3389
|
+
...notes !== void 0 ? { notes } : {},
|
|
3211
3390
|
...error !== void 0 ? { error } : {}
|
|
3212
3391
|
};
|
|
3213
3392
|
}
|
|
@@ -3497,7 +3676,7 @@ function makeUpdateRoutes(deps) {
|
|
|
3497
3676
|
//#region src/mount-once.ts
|
|
3498
3677
|
/**
|
|
3499
3678
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
3500
|
-
* (dsh-web-
|
|
3679
|
+
* (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
3501
3680
|
* the loader accepts a standalone install of the same package side by side;
|
|
3502
3681
|
* without this guard the second instance would still re-register the same
|
|
3503
3682
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
@@ -3510,7 +3689,7 @@ function makeUpdateRoutes(deps) {
|
|
|
3510
3689
|
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
3511
3690
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
3512
3691
|
*/
|
|
3513
|
-
const MOUNTED = Symbol.for("dsh-web
|
|
3692
|
+
const MOUNTED = Symbol.for("dsh-web.mounted-plugins");
|
|
3514
3693
|
function mountedSet() {
|
|
3515
3694
|
const registry = globalThis;
|
|
3516
3695
|
return registry[MOUNTED] ??= /* @__PURE__ */ new Set();
|
|
@@ -3628,7 +3807,12 @@ const UUID_POLYFILL_SCRIPT = [
|
|
|
3628
3807
|
/** Stable cordis plugin name. */
|
|
3629
3808
|
const name = "remote-web-ui";
|
|
3630
3809
|
/** Services required before the pairing surfaces can mount. */
|
|
3631
|
-
const inject = [
|
|
3810
|
+
const inject = [
|
|
3811
|
+
"webServer",
|
|
3812
|
+
"apiProxy",
|
|
3813
|
+
"commands",
|
|
3814
|
+
"agents"
|
|
3815
|
+
];
|
|
3632
3816
|
/**
|
|
3633
3817
|
* Settings namespace of the remote-control capability — the section the web
|
|
3634
3818
|
* settings surface edits. Spelled here rather than imported: the browser
|
|
@@ -3765,6 +3949,17 @@ function applyImpl(ctx, config) {
|
|
|
3765
3949
|
return;
|
|
3766
3950
|
}
|
|
3767
3951
|
});
|
|
3952
|
+
const releaseNotesCache = /* @__PURE__ */ new Map();
|
|
3953
|
+
const fetchReleaseNotesCached = async (version) => {
|
|
3954
|
+
const cached = releaseNotesCache.get(version);
|
|
3955
|
+
if (cached !== void 0 && Date.now() - cached.at < 6e5) return cached.notes;
|
|
3956
|
+
const notes = await fetchGitHubReleaseNotes(version, fetch);
|
|
3957
|
+
releaseNotesCache.set(version, {
|
|
3958
|
+
at: Date.now(),
|
|
3959
|
+
notes
|
|
3960
|
+
});
|
|
3961
|
+
return notes;
|
|
3962
|
+
};
|
|
3768
3963
|
const updateRoutes = makeUpdateRoutes({
|
|
3769
3964
|
fence: (request) => isTrustedApiRequest(request, []),
|
|
3770
3965
|
check: () => checkUpdates({
|
|
@@ -3776,7 +3971,8 @@ function applyImpl(ctx, config) {
|
|
|
3776
3971
|
return;
|
|
3777
3972
|
}
|
|
3778
3973
|
},
|
|
3779
|
-
fetchLatest: (name) => fetchLatestVersion(name, fetch)
|
|
3974
|
+
fetchLatest: (name) => fetchLatestVersion(name, fetch),
|
|
3975
|
+
fetchReleaseNotes: fetchReleaseNotesCached
|
|
3780
3976
|
}),
|
|
3781
3977
|
run: async () => {
|
|
3782
3978
|
const target = resolveUpdateTarget({ anchorManifestPath: resolveAnchorPath() });
|
|
@@ -3786,7 +3982,7 @@ function applyImpl(ctx, config) {
|
|
|
3786
3982
|
ok: false,
|
|
3787
3983
|
exitCode: null,
|
|
3788
3984
|
output: "",
|
|
3789
|
-
error: code === "not-found" ? "dsh-web
|
|
3985
|
+
error: code === "not-found" ? "dsh-web aggregate not installed" : "local link install — update unavailable",
|
|
3790
3986
|
errorCode: code
|
|
3791
3987
|
};
|
|
3792
3988
|
}
|
|
@@ -3804,7 +4000,8 @@ function applyImpl(ctx, config) {
|
|
|
3804
4000
|
return;
|
|
3805
4001
|
}
|
|
3806
4002
|
},
|
|
3807
|
-
fetchLatest: (name) => fetchLatestVersion(name, fetch)
|
|
4003
|
+
fetchLatest: (name) => fetchLatestVersion(name, fetch),
|
|
4004
|
+
fetchReleaseNotes: fetchReleaseNotesCached
|
|
3808
4005
|
}
|
|
3809
4006
|
});
|
|
3810
4007
|
}
|
|
@@ -3820,7 +4017,17 @@ function applyImpl(ctx, config) {
|
|
|
3820
4017
|
service,
|
|
3821
4018
|
apiProxy,
|
|
3822
4019
|
pendingTracker: new PendingTracker(),
|
|
3823
|
-
mobileEnterToSend: () => resolve().mobileEnterToSend
|
|
4020
|
+
mobileEnterToSend: () => resolve().mobileEnterToSend,
|
|
4021
|
+
commandDispatcher: ctx.commands !== void 0 && ctx.agents !== void 0 ? { async execute(sessionId, line, signal) {
|
|
4022
|
+
const agent = ctx.agents.get(sessionId);
|
|
4023
|
+
if (agent === void 0) return { error: "session-not-found" };
|
|
4024
|
+
const execution = await ctx.commands.execute(agent, line, [], signal);
|
|
4025
|
+
if (execution === void 0) return { error: "unknown-command" };
|
|
4026
|
+
return {
|
|
4027
|
+
ok: true,
|
|
4028
|
+
result: execution.result
|
|
4029
|
+
};
|
|
4030
|
+
} } : void 0
|
|
3824
4031
|
}) : [],
|
|
3825
4032
|
...apiProxy !== void 0 ? makePairedModelCatalogRoutes({
|
|
3826
4033
|
service,
|
package/lib/mobile.js
CHANGED
|
@@ -7610,15 +7610,18 @@ async function prompt(sessionId, text) {
|
|
|
7610
7610
|
}
|
|
7611
7611
|
/** Send one slash command line (e.g. `/permission workspace-write`). */
|
|
7612
7612
|
async function sendCommand(sessionId, line) {
|
|
7613
|
-
return await callUnary("
|
|
7613
|
+
return await callUnary("mobile.command", {
|
|
7614
7614
|
sessionId,
|
|
7615
|
-
|
|
7616
|
-
content: [{
|
|
7617
|
-
type: "text",
|
|
7618
|
-
text: line
|
|
7619
|
-
}]
|
|
7615
|
+
line
|
|
7620
7616
|
});
|
|
7621
7617
|
}
|
|
7618
|
+
/**
|
|
7619
|
+
* Stop the session's active turn (the mobile stop button). Pending queued
|
|
7620
|
+
* work is preserved and resumes in FIFO order once cancellation settles.
|
|
7621
|
+
*/
|
|
7622
|
+
async function cancelSession(sessionId) {
|
|
7623
|
+
return await callUnary("session.cancel", { sessionId });
|
|
7624
|
+
}
|
|
7622
7625
|
/** Fresh advisory model directory for one session (current + groups + failures). */
|
|
7623
7626
|
async function models(sessionId) {
|
|
7624
7627
|
return await callUnary("session.models", { sessionId });
|
|
@@ -14039,6 +14042,8 @@ function ChatView({ session, mux, onBack }) {
|
|
|
14039
14042
|
const [mobileEnterToSend, setMobileEnterToSend] = (0, import_react.useState)(true);
|
|
14040
14043
|
/** Whether the assistant is currently generating (turn/start..turn/end). */
|
|
14041
14044
|
const [running, setRunning] = (0, import_react.useState)(false);
|
|
14045
|
+
/** Whether a stop request is in flight (guards the composer's stop button). */
|
|
14046
|
+
const [stopping, setStopping] = (0, import_react.useState)(false);
|
|
14042
14047
|
/** Pending tool approvals awaiting user decision (#1025). */
|
|
14043
14048
|
const [pendingApprovals, setPendingApprovals] = (0, import_react.useState)([]);
|
|
14044
14049
|
/** Pending questions awaiting user answer (#1025). */
|
|
@@ -14245,6 +14250,22 @@ function ChatView({ session, mux, onBack }) {
|
|
|
14245
14250
|
sending,
|
|
14246
14251
|
session.sessionId
|
|
14247
14252
|
]);
|
|
14253
|
+
/**
|
|
14254
|
+
* Stop the active turn (desktop parity: the composer's primary button
|
|
14255
|
+
* becomes a stop button while running). The turn/end frame arriving over
|
|
14256
|
+
* mux flips the button back; a failed request surfaces through the chat
|
|
14257
|
+
* error line.
|
|
14258
|
+
*/
|
|
14259
|
+
const stopTurn = (0, import_react.useCallback)(() => {
|
|
14260
|
+
if (stopping) return;
|
|
14261
|
+
setStopping(true);
|
|
14262
|
+
cancelSession(session.sessionId).then(() => {
|
|
14263
|
+
setStopping(false);
|
|
14264
|
+
}, (reason) => {
|
|
14265
|
+
setStopping(false);
|
|
14266
|
+
setError(errorText(reason));
|
|
14267
|
+
});
|
|
14268
|
+
}, [stopping, session.sessionId]);
|
|
14248
14269
|
const modelLabel = currentModel?.model ?? "模型";
|
|
14249
14270
|
const permissionLabel = permissions === void 0 ? void 0 : permissions.options.find((option) => option.value === permissions.currentValue)?.name ?? displayName(permissions.currentValue);
|
|
14250
14271
|
const contextUsage = (0, import_react.useMemo)(() => {
|
|
@@ -14434,12 +14455,28 @@ function ChatView({ session, mux, onBack }) {
|
|
|
14434
14455
|
}
|
|
14435
14456
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
14436
14457
|
type: "button",
|
|
14437
|
-
className: "chat-send",
|
|
14438
|
-
|
|
14458
|
+
className: running ? "chat-send chat-send-stop" : "chat-send",
|
|
14459
|
+
...running ? { "aria-label": stopping ? "停止中" : "停止" } : {},
|
|
14460
|
+
disabled: running ? stopping : sending || input.trim() === "",
|
|
14439
14461
|
onClick: () => {
|
|
14440
|
-
|
|
14462
|
+
if (running) stopTurn();
|
|
14463
|
+
else send();
|
|
14441
14464
|
},
|
|
14442
|
-
children:
|
|
14465
|
+
children: running ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", {
|
|
14466
|
+
viewBox: "0 0 16 16",
|
|
14467
|
+
width: "14",
|
|
14468
|
+
height: "14",
|
|
14469
|
+
"aria-hidden": "true",
|
|
14470
|
+
focusable: "false",
|
|
14471
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", {
|
|
14472
|
+
x: "3",
|
|
14473
|
+
y: "3",
|
|
14474
|
+
width: "10",
|
|
14475
|
+
height: "10",
|
|
14476
|
+
rx: "3",
|
|
14477
|
+
fill: "currentColor"
|
|
14478
|
+
})
|
|
14479
|
+
}) : sending ? "发送中…" : "发送"
|
|
14443
14480
|
})]
|
|
14444
14481
|
}),
|
|
14445
14482
|
sheet === "model" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModelSheet, {
|
|
@@ -14490,6 +14527,11 @@ function ChatView({ session, mux, onBack }) {
|
|
|
14490
14527
|
*/
|
|
14491
14528
|
const MessageRow = (0, import_react.memo)(function MessageRow({ message, showToolCalls, showSystemMessages }) {
|
|
14492
14529
|
if (message.kind === "user" && message.sourceKind !== void 0 && message.sourceKind !== "user" && !showSystemMessages) return null;
|
|
14530
|
+
const hasReasoning = message.kind === "assistant" && message.reasoning !== void 0 && message.reasoning !== "";
|
|
14531
|
+
const hasTools = showToolCalls && message.kind === "assistant" && message.tools !== void 0 && message.tools.length > 0;
|
|
14532
|
+
const hasText = message.text !== "";
|
|
14533
|
+
const hasFailTag = message.failed === true;
|
|
14534
|
+
if (!hasReasoning && !hasTools && !hasText && !hasFailTag) return null;
|
|
14493
14535
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
14494
14536
|
className: `chat-msg chat-msg-${message.kind}${message.pending === true ? " chat-msg-pending" : ""}${message.failed === true ? " chat-msg-failed" : ""}`,
|
|
14495
14537
|
children: [
|
|
@@ -14648,7 +14690,7 @@ function MarkdownText({ text, pending }) {
|
|
|
14648
14690
|
if (timerRef.current !== void 0) clearTimeout(timerRef.current);
|
|
14649
14691
|
};
|
|
14650
14692
|
}, []);
|
|
14651
|
-
const long = text.length >
|
|
14693
|
+
const long = !pending && text.length > 6e3;
|
|
14652
14694
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
14653
14695
|
className: "chat-msg-text chat-md" + (long && !open ? " chat-md-collapsed" : ""),
|
|
14654
14696
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
@@ -14667,7 +14709,7 @@ function MarkdownText({ text, pending }) {
|
|
|
14667
14709
|
/** Long assistant text collapses behind an explicit expand toggle. */
|
|
14668
14710
|
function CollapsibleText({ text }) {
|
|
14669
14711
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
14670
|
-
if (text.length <=
|
|
14712
|
+
if (text.length <= 6e3) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
14671
14713
|
className: "chat-msg-text",
|
|
14672
14714
|
children: text
|
|
14673
14715
|
});
|
|
@@ -14687,7 +14729,6 @@ function CollapsibleText({ text }) {
|
|
|
14687
14729
|
]
|
|
14688
14730
|
});
|
|
14689
14731
|
}
|
|
14690
|
-
const LONG_TEXT_LIMIT = 1600;
|
|
14691
14732
|
const LONG_TEXT_PREVIEW = 800;
|
|
14692
14733
|
/** Latest non-empty line of a streaming reasoning buffer. */
|
|
14693
14734
|
function lastLine(text) {
|
|
@@ -17270,6 +17311,16 @@ body {
|
|
|
17270
17311
|
opacity: 0.5;
|
|
17271
17312
|
}
|
|
17272
17313
|
|
|
17314
|
+
/* Running state: the primary button carries only the square stop icon, so
|
|
17315
|
+
it centers the glyph instead of sizing to text. */
|
|
17316
|
+
.chat-send-stop {
|
|
17317
|
+
display: inline-flex;
|
|
17318
|
+
align-items: center;
|
|
17319
|
+
justify-content: center;
|
|
17320
|
+
min-width: 44px;
|
|
17321
|
+
padding: 0 12px;
|
|
17322
|
+
}
|
|
17323
|
+
|
|
17273
17324
|
.chat-load-older {
|
|
17274
17325
|
align-self: center;
|
|
17275
17326
|
margin: 4px 0;
|