@linxin666/dsh-remote-web-ui 0.2.0 → 0.2.2
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 +17 -8
- package/README.zh.md +5 -5
- package/lib/client.js +259 -43
- package/lib/client.js.map +1 -1
- package/lib/index.js +26 -7
- package/lib/mobile.js +209 -39
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +22 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +142 -3
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +11 -0
- package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +8 -5
- package/lib/types/client/UpdatePanel.d.ts +3 -1
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +3 -3
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +8 -4
- package/lib/types/client/pair-api.d.ts +1 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +1 -10
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +2 -0
- package/lib/types/mobile/api.d.ts +10 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +4 -0
- package/lib/types/mobile/messages.d.ts +24 -0
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +60 -23
- package/lib/types/mobile/messages.test.js +49 -1
- 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 +38 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +34 -11
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +30 -4
- package/lib/types/mobile/views/SessionListView.test.js +58 -1
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +4 -2
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +10 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +22 -2
- package/lib/types/tunnel.d.ts +1 -0
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -2
- package/lib/types/update.d.ts +1 -1
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +1 -1
- package/package.json +1 -2
- package/src/client/PluginSettingsCard.tsx +219 -18
- package/src/client/RemoteEntry.tsx +10 -0
- package/src/client/RemoteSettingsCard.tsx +2 -2
- package/src/client/UpdateEntry.tsx +9 -5
- package/src/client/UpdatePanel.tsx +12 -1
- package/src/client/locales.ts +8 -4
- package/src/client/pair-api.ts +1 -10
- package/src/client/settings-card.module.css +90 -1
- package/src/gate.ts +2 -0
- package/src/mobile/api.ts +14 -1
- package/src/mobile/messages.test.ts +52 -1
- package/src/mobile/messages.ts +65 -22
- package/src/mobile/mobile-styles.ts +38 -0
- package/src/mobile/mobile-theme.test.ts +1 -1
- package/src/mobile/views/ChatView.tsx +33 -10
- package/src/mobile/views/SessionListView.test.tsx +69 -1
- package/src/mobile/views/SessionListView.tsx +58 -5
- package/src/mobile/views/WorkspaceView.tsx +4 -2
- package/src/mobile-api.ts +9 -1
- package/src/routes.ts +22 -2
- package/src/tunnel.ts +7 -2
- package/src/update.ts +2 -2
package/lib/index.js
CHANGED
|
@@ -868,7 +868,9 @@ function makeRoutes(deps) {
|
|
|
868
868
|
const ACCEPT_MAX_ATTEMPTS = 10;
|
|
869
869
|
const ACCEPT_WINDOW_MS = 3e4;
|
|
870
870
|
const rateLimitAccept = (req) => {
|
|
871
|
-
const
|
|
871
|
+
const socketIp = req.socket?.remoteAddress ?? "unknown";
|
|
872
|
+
const forwarded = typeof req.headers["x-forwarded-for"] === "string" ? (req.headers["x-forwarded-for"].split(",")[0] ?? "").trim() : void 0;
|
|
873
|
+
const ip = forwarded === void 0 || forwarded === "" ? socketIp : socketIp + "|" + forwarded;
|
|
872
874
|
const nowMs = Date.now();
|
|
873
875
|
if (acceptAttempts.size > 256) {
|
|
874
876
|
for (const [key, attempt] of acceptAttempts) if (nowMs - attempt.windowStart > ACCEPT_WINDOW_MS) acceptAttempts.delete(key);
|
|
@@ -1022,10 +1024,16 @@ function makeRoutes(deps) {
|
|
|
1022
1024
|
return;
|
|
1023
1025
|
}
|
|
1024
1026
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
1027
|
+
const paired = deviceId !== void 0 && service.hasDevice(deviceId);
|
|
1028
|
+
const snapshot = service.snapshot();
|
|
1025
1029
|
writeJson(res, 200, {
|
|
1026
1030
|
ok: true,
|
|
1027
|
-
paired
|
|
1028
|
-
...
|
|
1031
|
+
paired,
|
|
1032
|
+
...paired ? snapshot : {
|
|
1033
|
+
phase: snapshot.phase,
|
|
1034
|
+
lanAvailable: snapshot.lanAvailable,
|
|
1035
|
+
lanAddresses: snapshot.lanAddresses
|
|
1036
|
+
}
|
|
1029
1037
|
});
|
|
1030
1038
|
};
|
|
1031
1039
|
const handleEvents = (req, res) => {
|
|
@@ -1306,9 +1314,16 @@ function makeMobileRoutes() {
|
|
|
1306
1314
|
}
|
|
1307
1315
|
//#endregion
|
|
1308
1316
|
//#region src/mobile-api.ts
|
|
1309
|
-
/**
|
|
1317
|
+
/**
|
|
1318
|
+
* Methods the phone surface may call. Everything else is refused HERE — but
|
|
1319
|
+
* note the paired-device cookie also passes the global api/gate for the full
|
|
1320
|
+
* ApiProxy surface (gate.ts), so a paired phone is a full-control credential:
|
|
1321
|
+
* the allowlist only constrains this /m/api proxy, not the cookie's reach.
|
|
1322
|
+
* stop() revokes every device; there is no per-device revocation yet.
|
|
1323
|
+
*/
|
|
1310
1324
|
const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1311
1325
|
"workspace.list",
|
|
1326
|
+
"agentPreset.list",
|
|
1312
1327
|
"session.create",
|
|
1313
1328
|
"session.list",
|
|
1314
1329
|
"session.history",
|
|
@@ -1573,6 +1588,7 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
|
1573
1588
|
result: response.result
|
|
1574
1589
|
});
|
|
1575
1590
|
if (method === "workspace.list") return wrap(await apiProxy.workspace.list(request));
|
|
1591
|
+
if (method === "agentPreset.list") return wrap(await apiProxy.agentPresets.list(request));
|
|
1576
1592
|
if (method === "session.create") return wrap(await apiProxy.sessions.create(request));
|
|
1577
1593
|
if (method === "session.history") return wrap(await apiProxy.sessions.history(request));
|
|
1578
1594
|
if (method === "session.search") return wrap(await apiProxy.sessions.search(request, signal ?? new AbortController().signal));
|
|
@@ -1650,6 +1666,7 @@ var TunnelManager = class {
|
|
|
1650
1666
|
urlTimer;
|
|
1651
1667
|
restartTimer;
|
|
1652
1668
|
attempts = 0;
|
|
1669
|
+
generation = 0;
|
|
1653
1670
|
stopping = false;
|
|
1654
1671
|
urlListeners = /* @__PURE__ */ new Set();
|
|
1655
1672
|
phaseListeners = /* @__PURE__ */ new Set();
|
|
@@ -1684,6 +1701,7 @@ var TunnelManager = class {
|
|
|
1684
1701
|
this.stopping = false;
|
|
1685
1702
|
this.targetUrl = targetUrl;
|
|
1686
1703
|
this.attempts = 0;
|
|
1704
|
+
this.generation += 1;
|
|
1687
1705
|
this.attempt();
|
|
1688
1706
|
}
|
|
1689
1707
|
/** Stop the tunnel for good: no restarts, no state. */
|
|
@@ -1713,12 +1731,13 @@ var TunnelManager = class {
|
|
|
1713
1731
|
}
|
|
1714
1732
|
attempt() {
|
|
1715
1733
|
if (this.stopping || this.targetUrl === void 0) return;
|
|
1734
|
+
const gen = this.generation;
|
|
1716
1735
|
this.setPhase("starting");
|
|
1717
1736
|
this.handle = void 0;
|
|
1718
1737
|
this.url = void 0;
|
|
1719
1738
|
this.error = void 0;
|
|
1720
1739
|
this.ensureBinary().then(() => {
|
|
1721
|
-
if (this.stopping || this.targetUrl === void 0) return;
|
|
1740
|
+
if (this.stopping || this.targetUrl === void 0 || gen !== this.generation) return;
|
|
1722
1741
|
const handle = this.factory(this.targetUrl);
|
|
1723
1742
|
this.handle = handle;
|
|
1724
1743
|
this.urlTimer = this.timer.setTimeout(() => {
|
|
@@ -1737,7 +1756,7 @@ var TunnelManager = class {
|
|
|
1737
1756
|
this.error = value instanceof Error ? value.message : String(value);
|
|
1738
1757
|
});
|
|
1739
1758
|
}).catch((value) => {
|
|
1740
|
-
if (this.stopping || this.targetUrl === void 0) return;
|
|
1759
|
+
if (this.stopping || this.targetUrl === void 0 || gen !== this.generation) return;
|
|
1741
1760
|
const message = value instanceof Error ? value.message : String(value);
|
|
1742
1761
|
this.fail(`could not obtain the cloudflared binary: ${message}`);
|
|
1743
1762
|
});
|
|
@@ -1968,7 +1987,7 @@ async function fetchLatestVersion(name, fetchImpl, timeoutMs = 1e4) {
|
|
|
1968
1987
|
controller.abort();
|
|
1969
1988
|
}, timeoutMs);
|
|
1970
1989
|
try {
|
|
1971
|
-
const response = await fetchImpl("https://registry.npmjs.org/" + name.replace("/", "%2F") + "/latest");
|
|
1990
|
+
const response = await fetchImpl("https://registry.npmjs.org/" + name.replace("/", "%2F") + "/latest", { signal: controller.signal });
|
|
1972
1991
|
if (!response.ok) return void 0;
|
|
1973
1992
|
const body = await response.json();
|
|
1974
1993
|
if (typeof body !== "object" || body === null) return void 0;
|
package/lib/mobile.js
CHANGED
|
@@ -7578,6 +7578,10 @@ async function fetchMobilePreferences() {
|
|
|
7578
7578
|
async function listSessions(cursor) {
|
|
7579
7579
|
return await callUnary("session.list", cursor === void 0 ? {} : { cursor });
|
|
7580
7580
|
}
|
|
7581
|
+
/** Read the available agent compositions for a new session. */
|
|
7582
|
+
async function listAgentPresets() {
|
|
7583
|
+
return await callUnary("agentPreset.list", {});
|
|
7584
|
+
}
|
|
7581
7585
|
/**
|
|
7582
7586
|
* Create a blank session (entity birth precedes the first message). Name a
|
|
7583
7587
|
* workspace to attach it there, or a cwd; omitting both uses the host cwd.
|
|
@@ -13052,13 +13056,6 @@ function chunkTarget(data) {
|
|
|
13052
13056
|
if (step !== void 0) result.step = step;
|
|
13053
13057
|
return result;
|
|
13054
13058
|
}
|
|
13055
|
-
/** Max message seq across an existing list, or -1 for an empty list. */
|
|
13056
|
-
function watermarkOf(existing) {
|
|
13057
|
-
if (existing === void 0) return -1;
|
|
13058
|
-
let maxSeq = -1;
|
|
13059
|
-
for (const message of existing) if (message.seq > maxSeq) maxSeq = message.seq;
|
|
13060
|
-
return maxSeq;
|
|
13061
|
-
}
|
|
13062
13059
|
function createState(existing) {
|
|
13063
13060
|
const messages = existing === void 0 ? [] : [...existing];
|
|
13064
13061
|
const state = {
|
|
@@ -13067,9 +13064,11 @@ function createState(existing) {
|
|
|
13067
13064
|
pendingByTurnStep: /* @__PURE__ */ new Map(),
|
|
13068
13065
|
turnStepMessage: /* @__PURE__ */ new Map(),
|
|
13069
13066
|
messageTurn: /* @__PURE__ */ new Map(),
|
|
13070
|
-
toolNames: /* @__PURE__ */ new Map()
|
|
13067
|
+
toolNames: /* @__PURE__ */ new Map(),
|
|
13068
|
+
maxSeq: -1
|
|
13071
13069
|
};
|
|
13072
13070
|
for (const message of messages) {
|
|
13071
|
+
if (message.seq > state.maxSeq) state.maxSeq = message.seq;
|
|
13073
13072
|
state.byId.set(message.id, message);
|
|
13074
13073
|
if (message.kind !== "assistant") continue;
|
|
13075
13074
|
const decoded = decodePendingTurnStep(message.id);
|
|
@@ -13123,6 +13122,7 @@ function retargetTurnStep(state, key, oldMessage, next) {
|
|
|
13123
13122
|
}
|
|
13124
13123
|
/** Fold one event into the working state. Assumes the event passes the watermark. */
|
|
13125
13124
|
function applyEvent(state, event) {
|
|
13125
|
+
if (event.seq > state.maxSeq) state.maxSeq = event.seq;
|
|
13126
13126
|
switch (event.type) {
|
|
13127
13127
|
case "user/message":
|
|
13128
13128
|
applyUserMessage(state, event);
|
|
@@ -13417,14 +13417,58 @@ function applyTurnEnd(state, event) {
|
|
|
13417
13417
|
* @returns messages sorted by seq.
|
|
13418
13418
|
*/
|
|
13419
13419
|
function foldEvents(events, existing) {
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13426
|
-
|
|
13427
|
-
|
|
13420
|
+
return new EventFolder(existing).fold(events);
|
|
13421
|
+
}
|
|
13422
|
+
/**
|
|
13423
|
+
* Incremental folder for one message stream. Live chat folds one event at a
|
|
13424
|
+
* time; rebuilding the five index maps by scanning every message per event
|
|
13425
|
+
* made that path O(n) per event (O(n * events) per turn). A folder keeps the
|
|
13426
|
+
* indexes alive across folds, applies each event in O(1) map operations, and
|
|
13427
|
+
* returns the previous snapshot identity unchanged when nothing applied, so
|
|
13428
|
+
* React skips the re-render entirely. Replayed events are no-ops: the maxSeq
|
|
13429
|
+
* watermark advanced by the first application skips them, which also makes a
|
|
13430
|
+
* double-invoked React state updater harmless.
|
|
13431
|
+
*/
|
|
13432
|
+
var EventFolder = class {
|
|
13433
|
+
state;
|
|
13434
|
+
snapshotList;
|
|
13435
|
+
/** @param initial - seed rows (history tail load); omit for an empty stream. */
|
|
13436
|
+
constructor(initial) {
|
|
13437
|
+
this.state = createState(initial);
|
|
13438
|
+
}
|
|
13439
|
+
/** Fold one batch incrementally; returns the current snapshot list. */
|
|
13440
|
+
fold(events) {
|
|
13441
|
+
const sorted = [...events].sort((a, b) => a.seq - b.seq);
|
|
13442
|
+
let applied = false;
|
|
13443
|
+
for (const event of sorted) {
|
|
13444
|
+
if (event.seq <= this.state.maxSeq) continue;
|
|
13445
|
+
applyEvent(this.state, event);
|
|
13446
|
+
applied = true;
|
|
13447
|
+
}
|
|
13448
|
+
if (!applied && this.snapshotList !== void 0) return this.snapshotList;
|
|
13449
|
+
this.snapshotList = snapshotOf(this.state);
|
|
13450
|
+
return this.snapshotList;
|
|
13451
|
+
}
|
|
13452
|
+
/** Replace the whole stream (history reload / session switch). */
|
|
13453
|
+
seed(messages) {
|
|
13454
|
+
this.state = createState(messages);
|
|
13455
|
+
this.snapshotList = void 0;
|
|
13456
|
+
}
|
|
13457
|
+
/** Prepend an older history page (exact seam; no overlapping seqs). */
|
|
13458
|
+
prepend(older) {
|
|
13459
|
+
this.state = createState([...older, ...this.state.messages]);
|
|
13460
|
+
this.snapshotList = void 0;
|
|
13461
|
+
}
|
|
13462
|
+
/** Current snapshot list; a fresh copy whenever the folder changed. */
|
|
13463
|
+
snapshot() {
|
|
13464
|
+
if (this.snapshotList !== void 0) return this.snapshotList;
|
|
13465
|
+
this.snapshotList = snapshotOf(this.state);
|
|
13466
|
+
return this.snapshotList;
|
|
13467
|
+
}
|
|
13468
|
+
};
|
|
13469
|
+
/** Copy the folder's rows and keep them seq-ordered (skips re-sorting the common ordered case). */
|
|
13470
|
+
function snapshotOf(state) {
|
|
13471
|
+
const out = [...state.messages];
|
|
13428
13472
|
let ordered = true;
|
|
13429
13473
|
for (let index = 1; index < out.length; index += 1) {
|
|
13430
13474
|
const prev = out[index - 1];
|
|
@@ -13434,7 +13478,7 @@ function foldEvents(events, existing) {
|
|
|
13434
13478
|
break;
|
|
13435
13479
|
}
|
|
13436
13480
|
}
|
|
13437
|
-
return ordered ? out :
|
|
13481
|
+
return ordered ? out : out.sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
|
|
13438
13482
|
}
|
|
13439
13483
|
//#endregion
|
|
13440
13484
|
//#region src/mobile/markdown.ts
|
|
@@ -13911,6 +13955,8 @@ function ChatView({ session, mux, onBack }) {
|
|
|
13911
13955
|
const tailLoadingRef = (0, import_react.useRef)(true);
|
|
13912
13956
|
/** Live session events buffered while the initial tail page loads. */
|
|
13913
13957
|
const liveBufferRef = (0, import_react.useRef)([]);
|
|
13958
|
+
/** Incremental folder for this session's stream (indexes stay hot across events). */
|
|
13959
|
+
const folderRef = (0, import_react.useRef)(void 0);
|
|
13914
13960
|
/** True once the live buffer hit its cap (oldest events were dropped). */
|
|
13915
13961
|
const liveBufferOverflowRef = (0, import_react.useRef)(false);
|
|
13916
13962
|
/** The session's permission select (absent = capability not composed). */
|
|
@@ -13946,6 +13992,7 @@ function ChatView({ session, mux, onBack }) {
|
|
|
13946
13992
|
tailLoadingRef.current = true;
|
|
13947
13993
|
liveBufferRef.current = [];
|
|
13948
13994
|
liveBufferOverflowRef.current = false;
|
|
13995
|
+
folderRef.current = void 0;
|
|
13949
13996
|
setLoading(true);
|
|
13950
13997
|
setError(void 0);
|
|
13951
13998
|
setMessages([]);
|
|
@@ -13954,14 +14001,18 @@ function ChatView({ session, mux, onBack }) {
|
|
|
13954
14001
|
const buffered = liveBufferRef.current;
|
|
13955
14002
|
liveBufferRef.current = [];
|
|
13956
14003
|
tailLoadingRef.current = false;
|
|
13957
|
-
|
|
14004
|
+
const folder = new EventFolder(foldEvents(page.events.map(eventOf)));
|
|
14005
|
+
folderRef.current = folder;
|
|
14006
|
+
setMessages(folder.fold(buffered));
|
|
13958
14007
|
setHasOlder(page.hasMore);
|
|
13959
14008
|
setLoading(false);
|
|
13960
14009
|
const projections = page.projections?.values;
|
|
13961
14010
|
setPermissions(parsePermissionSelect(projections?.["permissions"]));
|
|
13962
14011
|
if (liveBufferOverflowRef.current) loadHistory(session.sessionId, void 0, controller.signal).then((fresh) => {
|
|
13963
14012
|
if (cancelled) return;
|
|
13964
|
-
|
|
14013
|
+
const folder = folderRef.current;
|
|
14014
|
+
const freshEvents = fresh.events.map(eventOf);
|
|
14015
|
+
setMessages((previous) => folder === void 0 ? foldEvents(freshEvents, previous) : folder.fold(freshEvents));
|
|
13965
14016
|
liveBufferOverflowRef.current = false;
|
|
13966
14017
|
}, (reason) => {
|
|
13967
14018
|
if (cancelled) return;
|
|
@@ -13972,7 +14023,10 @@ function ChatView({ session, mux, onBack }) {
|
|
|
13972
14023
|
const buffered = liveBufferRef.current;
|
|
13973
14024
|
liveBufferRef.current = [];
|
|
13974
14025
|
tailLoadingRef.current = false;
|
|
13975
|
-
if (buffered.length > 0)
|
|
14026
|
+
if (buffered.length > 0) {
|
|
14027
|
+
const folder = folderRef.current;
|
|
14028
|
+
setMessages(folder === void 0 ? foldEvents(buffered) : folder.fold(buffered));
|
|
14029
|
+
}
|
|
13976
14030
|
setError(errorText(reason));
|
|
13977
14031
|
setLoading(false);
|
|
13978
14032
|
});
|
|
@@ -14002,7 +14056,10 @@ function ChatView({ session, mux, onBack }) {
|
|
|
14002
14056
|
liveBufferRef.current.push(event);
|
|
14003
14057
|
return;
|
|
14004
14058
|
}
|
|
14005
|
-
setMessages((previous) =>
|
|
14059
|
+
setMessages((previous) => {
|
|
14060
|
+
const folder = folderRef.current;
|
|
14061
|
+
return folder === void 0 ? foldEvents([event], previous) : folder.fold([event]);
|
|
14062
|
+
});
|
|
14006
14063
|
return;
|
|
14007
14064
|
}
|
|
14008
14065
|
if (frame.type === "session/projection" && frame.sessionId === session.sessionId && frame.key === "permissions") setPermissions(parsePermissionSelect(frame.value));
|
|
@@ -14039,7 +14096,12 @@ function ChatView({ session, mux, onBack }) {
|
|
|
14039
14096
|
pendingRef.current = false;
|
|
14040
14097
|
setLoading(false);
|
|
14041
14098
|
const older = foldEvents(page.events.map(eventOf));
|
|
14042
|
-
|
|
14099
|
+
const folder = folderRef.current;
|
|
14100
|
+
if (folder === void 0) setMessages((previous) => [...older, ...previous]);
|
|
14101
|
+
else {
|
|
14102
|
+
folder.prepend(older);
|
|
14103
|
+
setMessages(folder.snapshot());
|
|
14104
|
+
}
|
|
14043
14105
|
setHasOlder(page.hasMore);
|
|
14044
14106
|
}, (reason) => {
|
|
14045
14107
|
pendingRef.current = false;
|
|
@@ -14274,8 +14336,12 @@ function ChatView({ session, mux, onBack }) {
|
|
|
14274
14336
|
]
|
|
14275
14337
|
});
|
|
14276
14338
|
}
|
|
14277
|
-
/**
|
|
14278
|
-
|
|
14339
|
+
/**
|
|
14340
|
+
* One rendered message row (user bubble or assistant bubble with folds).
|
|
14341
|
+
* Memoized: live streaming updates exactly one message object per frame, so
|
|
14342
|
+
* unchanged rows skip re-rendering their markdown/sub-components.
|
|
14343
|
+
*/
|
|
14344
|
+
const MessageRow = (0, import_react.memo)(function MessageRow({ message, showToolCalls, showSystemMessages }) {
|
|
14279
14345
|
if (message.kind === "user" && message.sourceKind !== void 0 && message.sourceKind !== "user" && !showSystemMessages) return null;
|
|
14280
14346
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
14281
14347
|
className: `chat-msg chat-msg-${message.kind}${message.pending === true ? " chat-msg-pending" : ""}${message.failed === true ? " chat-msg-failed" : ""}`,
|
|
@@ -14296,7 +14362,7 @@ function MessageRow({ message, showToolCalls, showSystemMessages }) {
|
|
|
14296
14362
|
})
|
|
14297
14363
|
]
|
|
14298
14364
|
});
|
|
14299
|
-
}
|
|
14365
|
+
});
|
|
14300
14366
|
/** Collapsed-by-default reasoning disclosure (web-UI Think-row parity). */
|
|
14301
14367
|
function ReasoningDisclosure({ text, pending }) {
|
|
14302
14368
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
@@ -14848,6 +14914,9 @@ function SessionListView({ workspace, onBack, onPick }) {
|
|
|
14848
14914
|
const [error, setError] = (0, import_react.useState)(void 0);
|
|
14849
14915
|
const [creating, setCreating] = (0, import_react.useState)(false);
|
|
14850
14916
|
const [createError, setCreateError] = (0, import_react.useState)(void 0);
|
|
14917
|
+
const [presets, setPresets] = (0, import_react.useState)([]);
|
|
14918
|
+
const [selectedPreset, setSelectedPreset] = (0, import_react.useState)(void 0);
|
|
14919
|
+
const [presetsLoading, setPresetsLoading] = (0, import_react.useState)(true);
|
|
14851
14920
|
const cursorRef = (0, import_react.useRef)(void 0);
|
|
14852
14921
|
const busyRef = (0, import_react.useRef)(false);
|
|
14853
14922
|
(0, import_react.useEffect)(() => {
|
|
@@ -14870,6 +14939,25 @@ function SessionListView({ workspace, onBack, onPick }) {
|
|
|
14870
14939
|
cancelled = true;
|
|
14871
14940
|
};
|
|
14872
14941
|
}, [workspace]);
|
|
14942
|
+
(0, import_react.useEffect)(() => {
|
|
14943
|
+
let cancelled = false;
|
|
14944
|
+
setPresets([]);
|
|
14945
|
+
setSelectedPreset(void 0);
|
|
14946
|
+
setPresetsLoading(true);
|
|
14947
|
+
listAgentPresets().then((roster) => {
|
|
14948
|
+
if (cancelled) return;
|
|
14949
|
+
const usable = roster.presets.filter((preset) => preset.broken === void 0);
|
|
14950
|
+
setPresets(roster.presets);
|
|
14951
|
+
setSelectedPreset((usable.find((preset) => preset.isDefault) ?? usable[0])?.id);
|
|
14952
|
+
setPresetsLoading(false);
|
|
14953
|
+
}, () => {
|
|
14954
|
+
if (cancelled) return;
|
|
14955
|
+
setPresetsLoading(false);
|
|
14956
|
+
});
|
|
14957
|
+
return () => {
|
|
14958
|
+
cancelled = true;
|
|
14959
|
+
};
|
|
14960
|
+
}, [workspace]);
|
|
14873
14961
|
/** Pull the next page and append the rows that belong to this workspace. */
|
|
14874
14962
|
const loadMore = (0, import_react.useCallback)(() => {
|
|
14875
14963
|
if (busyRef.current) return;
|
|
@@ -14894,7 +14982,10 @@ function SessionListView({ workspace, onBack, onPick }) {
|
|
|
14894
14982
|
if (creating) return;
|
|
14895
14983
|
setCreating(true);
|
|
14896
14984
|
setCreateError(void 0);
|
|
14897
|
-
createSession({
|
|
14985
|
+
createSession({
|
|
14986
|
+
workspaceId: workspace.workspaceId,
|
|
14987
|
+
...selectedPreset !== void 0 ? { agentPreset: selectedPreset } : {}
|
|
14988
|
+
}).then((created) => {
|
|
14898
14989
|
setCreating(false);
|
|
14899
14990
|
const view = {
|
|
14900
14991
|
sessionId: created.sessionId,
|
|
@@ -14912,9 +15003,11 @@ function SessionListView({ workspace, onBack, onPick }) {
|
|
|
14912
15003
|
}, [
|
|
14913
15004
|
creating,
|
|
14914
15005
|
workspace,
|
|
14915
|
-
onPick
|
|
15006
|
+
onPick,
|
|
15007
|
+
selectedPreset
|
|
14916
15008
|
]);
|
|
14917
15009
|
const createHint = createError !== void 0 ? staleHostHint(createError) : void 0;
|
|
15010
|
+
const selectedPresetEntry = presets.find((preset) => preset.id === selectedPreset);
|
|
14918
15011
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
14919
15012
|
className: "mobile",
|
|
14920
15013
|
children: [
|
|
@@ -14939,17 +15032,50 @@ function SessionListView({ workspace, onBack, onPick }) {
|
|
|
14939
15032
|
className: "mobile-error mobile-pad",
|
|
14940
15033
|
children: error
|
|
14941
15034
|
}),
|
|
14942
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
14943
|
-
className: "mobile-pad",
|
|
14944
|
-
children:
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
15035
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
15036
|
+
className: "mobile-create mobile-pad",
|
|
15037
|
+
children: [
|
|
15038
|
+
presets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
|
|
15039
|
+
className: "mobile-preset",
|
|
15040
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
15041
|
+
className: "mobile-presetLabel",
|
|
15042
|
+
children: "Agent 模式"
|
|
15043
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
15044
|
+
className: "mobile-presetSelect",
|
|
15045
|
+
value: selectedPreset ?? "",
|
|
15046
|
+
disabled: selectedPreset === void 0,
|
|
15047
|
+
onChange: (event) => {
|
|
15048
|
+
setSelectedPreset(event.target.value);
|
|
15049
|
+
},
|
|
15050
|
+
children: [selectedPreset === void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
15051
|
+
value: "",
|
|
15052
|
+
children: "无可用 Agent 模式"
|
|
15053
|
+
}), presets.map((preset) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("option", {
|
|
15054
|
+
value: preset.id,
|
|
15055
|
+
disabled: preset.broken !== void 0,
|
|
15056
|
+
children: [
|
|
15057
|
+
preset.name ?? preset.id,
|
|
15058
|
+
preset.isDefault ? "(默认)" : "",
|
|
15059
|
+
preset.trust === "user" ? "(本地)" : "",
|
|
15060
|
+
preset.broken !== void 0 ? "(不可用)" : ""
|
|
15061
|
+
]
|
|
15062
|
+
}, preset.id))]
|
|
15063
|
+
})]
|
|
15064
|
+
}),
|
|
15065
|
+
selectedPresetEntry?.description !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
15066
|
+
className: "mobile-presetDescription",
|
|
15067
|
+
children: selectedPresetEntry.description
|
|
15068
|
+
}),
|
|
15069
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
15070
|
+
type: "button",
|
|
15071
|
+
className: "mobile-new",
|
|
15072
|
+
disabled: creating || presetsLoading,
|
|
15073
|
+
onClick: () => {
|
|
15074
|
+
handleCreate();
|
|
15075
|
+
},
|
|
15076
|
+
children: creating ? "创建中…" : "+ 新建会话"
|
|
15077
|
+
})
|
|
15078
|
+
]
|
|
14953
15079
|
}),
|
|
14954
15080
|
createError !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
14955
15081
|
className: "mobile-error mobile-pad",
|
|
@@ -15022,6 +15148,7 @@ function SessionListView({ workspace, onBack, onPick }) {
|
|
|
15022
15148
|
function WorkspaceView({ initialWorkspaceId, onPick }) {
|
|
15023
15149
|
const [items, setItems] = (0, import_react.useState)(void 0);
|
|
15024
15150
|
const [error, setError] = (0, import_react.useState)(void 0);
|
|
15151
|
+
const [reload, setReload] = (0, import_react.useState)(0);
|
|
15025
15152
|
(0, import_react.useEffect)(() => {
|
|
15026
15153
|
let cancelled = false;
|
|
15027
15154
|
listWorkspaces().then((rows) => {
|
|
@@ -15039,7 +15166,11 @@ function WorkspaceView({ initialWorkspaceId, onPick }) {
|
|
|
15039
15166
|
return () => {
|
|
15040
15167
|
cancelled = true;
|
|
15041
15168
|
};
|
|
15042
|
-
}, [
|
|
15169
|
+
}, [
|
|
15170
|
+
initialWorkspaceId,
|
|
15171
|
+
onPick,
|
|
15172
|
+
reload
|
|
15173
|
+
]);
|
|
15043
15174
|
if (error !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
15044
15175
|
className: "mobile",
|
|
15045
15176
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
|
|
@@ -15059,6 +15190,7 @@ function WorkspaceView({ initialWorkspaceId, onPick }) {
|
|
|
15059
15190
|
onClick: () => {
|
|
15060
15191
|
setError(void 0);
|
|
15061
15192
|
setItems(void 0);
|
|
15193
|
+
setReload((n) => n + 1);
|
|
15062
15194
|
},
|
|
15063
15195
|
children: "重试"
|
|
15064
15196
|
})]
|
|
@@ -15707,6 +15839,43 @@ body {
|
|
|
15707
15839
|
padding: 8px 16px;
|
|
15708
15840
|
}
|
|
15709
15841
|
|
|
15842
|
+
.mobile-create {
|
|
15843
|
+
display: grid;
|
|
15844
|
+
gap: 8px;
|
|
15845
|
+
}
|
|
15846
|
+
|
|
15847
|
+
.mobile-preset {
|
|
15848
|
+
display: grid;
|
|
15849
|
+
gap: 4px;
|
|
15850
|
+
}
|
|
15851
|
+
|
|
15852
|
+
.mobile-presetLabel {
|
|
15853
|
+
color: var(--m-text-secondary);
|
|
15854
|
+
font-size: 13px;
|
|
15855
|
+
font-weight: 600;
|
|
15856
|
+
}
|
|
15857
|
+
|
|
15858
|
+
.mobile-presetSelect {
|
|
15859
|
+
width: 100%;
|
|
15860
|
+
height: 40px;
|
|
15861
|
+
padding: 0 36px 0 12px;
|
|
15862
|
+
border: 1px solid var(--m-border);
|
|
15863
|
+
border-radius: 10px;
|
|
15864
|
+
background: var(--m-bg-input);
|
|
15865
|
+
color: var(--m-text);
|
|
15866
|
+
font: inherit;
|
|
15867
|
+
}
|
|
15868
|
+
|
|
15869
|
+
.mobile-presetSelect:disabled {
|
|
15870
|
+
opacity: 0.6;
|
|
15871
|
+
}
|
|
15872
|
+
|
|
15873
|
+
.mobile-presetDescription {
|
|
15874
|
+
margin: 0;
|
|
15875
|
+
color: var(--m-text-tertiary);
|
|
15876
|
+
font-size: 12px;
|
|
15877
|
+
}
|
|
15878
|
+
|
|
15710
15879
|
/* ── buttons ─────────────────────────────────────────────────────────── */
|
|
15711
15880
|
|
|
15712
15881
|
.mobile-button {
|
|
@@ -16351,6 +16520,7 @@ body {
|
|
|
16351
16520
|
.mobile-row:focus-visible,
|
|
16352
16521
|
.mobile-button:focus-visible,
|
|
16353
16522
|
.mobile-new:focus-visible,
|
|
16523
|
+
.mobile-presetSelect:focus-visible,
|
|
16354
16524
|
.chat-send:focus-visible,
|
|
16355
16525
|
.chat-chip:focus-visible,
|
|
16356
16526
|
.chat-msg-toggle:focus-visible,
|