@linxin666/dsh-remote-web-ui 0.3.2 → 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.
- package/README.i18n.yaml +2 -2
- package/README.md +22 -8
- package/README.zh.md +12 -7
- package/lib/client.js +174 -16
- package/lib/client.js.map +1 -1
- package/lib/index.js +104 -7
- package/lib/mobile.js +62 -7
- package/lib/mobile.js.map +1 -1
- 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/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 +14 -3
- 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 +7 -0
- 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.map +1 -1
- package/lib/types/mobile-api.js +3 -0
- 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 +2 -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-form.ts +1 -1
- package/src/client/telemetry.ts +105 -0
- package/src/http.ts +1 -1
- package/src/index.ts +16 -2
- package/src/loopback.ts +1 -1
- package/src/mobile/api.ts +8 -0
- 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 +2 -0
- package/src/mount-once.ts +2 -2
- 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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
14466
|
+
if (running) stopTurn();
|
|
14467
|
+
else send();
|
|
14441
14468
|
},
|
|
14442
|
-
children:
|
|
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 >
|
|
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 <=
|
|
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;
|