@linxin666/dsh-remote-web-ui 0.1.11 → 0.1.13
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 +6 -0
- package/README.md +12 -1
- package/README.zh.md +171 -0
- package/lib/client.js +170 -99
- package/lib/client.js.map +1 -1
- package/lib/index.js +224 -4
- package/lib/mobile.js +109 -33
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +3 -1
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +10 -2
- package/lib/types/client/deep-link.js +10 -5
- package/lib/types/client/index.d.ts +13 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +2 -1
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +8 -0
- package/lib/types/client/update-api.d.ts +10 -0
- package/lib/types/client/update-api.d.ts.map +1 -1
- package/lib/types/client/update-api.js +22 -2
- package/lib/types/index.d.ts +7 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +7 -1
- package/lib/types/mobile/api.d.ts +9 -2
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +7 -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 +3 -0
- package/lib/types/mobile/views/App.d.ts +3 -1
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +18 -3
- package/lib/types/mobile/views/ChatView.d.ts +6 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +95 -7
- package/lib/types/mobile/views/ChatView.test.js +161 -3
- package/lib/types/mobile/views/WorkspaceView.d.ts +3 -1
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +9 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +2 -0
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +1 -0
- package/lib/types/mobile/views/WorkspaceView.test.js +56 -0
- package/lib/types/mobile-api.d.ts +2 -0
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +17 -2
- package/package.json +2 -12
- package/src/client/PluginSettingsCard.tsx +30 -5
- package/src/client/RemoteSettingsCard.tsx +18 -0
- package/src/client/UpdateEntry.tsx +10 -2
- package/src/client/deep-link.ts +10 -4
- package/src/client/index.ts +15 -2
- package/src/client/locales.ts +8 -0
- package/src/client/settings-card.module.css +139 -144
- package/src/client/update-api.ts +22 -2
- package/src/index.ts +14 -1
- package/src/mobile/api.ts +14 -2
- package/src/mobile/mobile-styles.ts +3 -0
- package/src/mobile/views/App.tsx +22 -3
- package/src/mobile/views/ChatView.test.tsx +185 -3
- package/src/mobile/views/ChatView.tsx +102 -8
- package/src/mobile/views/WorkspaceView.test.tsx +66 -0
- package/src/mobile/views/WorkspaceView.tsx +11 -2
- package/src/mobile-api.ts +20 -2
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { setInterval as setInterval$1 } from "node:timers";
|
|
3
|
-
import {
|
|
3
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
4
4
|
import z from "schemastery";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
@@ -11,6 +11,203 @@ import { networkInterfaces } from "node:os";
|
|
|
11
11
|
import { Tunnel, bin, install } from "cloudflared";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-settings@0.1.0-rc.6_@deepseek-ai+cordis@4.0.1_@deepseek-ai+dsh-brand@0_52495af71738a1095726249ceff4fd7b/node_modules/@deepseek-ai/dsh-settings/lib/index.js
|
|
15
|
+
/**
|
|
16
|
+
* Structural secret redaction for settings values. `role('secret')` fields are
|
|
17
|
+
* removed from a value before it crosses a wire boundary; a sidecar records
|
|
18
|
+
* each schema-declared secret position and whether it currently holds a value,
|
|
19
|
+
* so a configuration surface can render a write-only input without ever
|
|
20
|
+
* receiving the secret itself.
|
|
21
|
+
* @module @deepseek-ai/dsh-settings/redact
|
|
22
|
+
*/
|
|
23
|
+
/** Whether a value is a plain data object the walker may recurse into. */
|
|
24
|
+
function isRecord(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
function walk(node, value, path, secrets) {
|
|
28
|
+
if (node === void 0) return value;
|
|
29
|
+
if (node.meta?.role === "secret") {
|
|
30
|
+
secrets.push({
|
|
31
|
+
path,
|
|
32
|
+
set: value !== void 0
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
switch (node.type) {
|
|
37
|
+
case "object": {
|
|
38
|
+
const properties = node.dict ?? {};
|
|
39
|
+
const source = isRecord(value) ? value : void 0;
|
|
40
|
+
const rebuilt = {};
|
|
41
|
+
if (source !== void 0) for (const [key, entry] of Object.entries(source)) {
|
|
42
|
+
if (key in properties) continue;
|
|
43
|
+
rebuilt[key] = entry;
|
|
44
|
+
}
|
|
45
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
46
|
+
const stripped = walk(child, source?.[key], [...path, key], secrets);
|
|
47
|
+
if (stripped !== void 0) rebuilt[key] = stripped;
|
|
48
|
+
}
|
|
49
|
+
return source === void 0 && Object.keys(rebuilt).length === 0 ? value : rebuilt;
|
|
50
|
+
}
|
|
51
|
+
case "dict": {
|
|
52
|
+
if (!isRecord(value)) return value;
|
|
53
|
+
const rebuilt = {};
|
|
54
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
55
|
+
const stripped = walk(node.inner, entry, [...path, key], secrets);
|
|
56
|
+
if (stripped !== void 0) rebuilt[key] = stripped;
|
|
57
|
+
}
|
|
58
|
+
return rebuilt;
|
|
59
|
+
}
|
|
60
|
+
case "array":
|
|
61
|
+
if (!Array.isArray(value)) return value;
|
|
62
|
+
return value.map((entry, index) => walk(node.inner, entry, [...path, String(index)], secrets));
|
|
63
|
+
default: return value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Service Definition for the user-settings capability seam (`ctx.settings`). Providers store one raw document of
|
|
68
|
+
* per-namespace sections; plugins register a namespace schema and read the
|
|
69
|
+
* resolved value, which layers schema defaults, the registrant's composition
|
|
70
|
+
* `base`, and the user document section, in that order.
|
|
71
|
+
* @module @deepseek-ai/dsh-settings
|
|
72
|
+
*/
|
|
73
|
+
const NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
74
|
+
/**
|
|
75
|
+
* Brand a raw string as a {@link SettingsNamespace}.
|
|
76
|
+
* @param value - candidate namespace; lowercase kebab-case, as in plugin short names.
|
|
77
|
+
* @returns the branded namespace.
|
|
78
|
+
*/
|
|
79
|
+
function settingsNamespace(value) {
|
|
80
|
+
if (!NAMESPACE_PATTERN.test(value)) throw new TypeError(`settings namespace "${value}" must match ${String(NAMESPACE_PATTERN)}`);
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Deep equality over JSON-compatible data (objects, arrays, primitives) — the
|
|
85
|
+
* Service Definition's single change-detection predicate, exported so the invariant
|
|
86
|
+
* companion checks exactly the implementation's relation.
|
|
87
|
+
* @param a - one JSON-compatible value.
|
|
88
|
+
* @param b - the other JSON-compatible value.
|
|
89
|
+
* @returns whether the two values are structurally equal.
|
|
90
|
+
*/
|
|
91
|
+
function deepEqualJson(a, b) {
|
|
92
|
+
if (a === b) return true;
|
|
93
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
94
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
95
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
96
|
+
return a.every((entry, index) => deepEqualJson(entry, b[index]));
|
|
97
|
+
}
|
|
98
|
+
const left = a;
|
|
99
|
+
const right = b;
|
|
100
|
+
const keys = Object.keys(left);
|
|
101
|
+
if (keys.length !== Object.keys(right).length) return false;
|
|
102
|
+
return keys.every((key) => key in right && deepEqualJson(left[key], right[key]));
|
|
103
|
+
}
|
|
104
|
+
/** Whether a value is a plain data object (not an array, null, or class instance). */
|
|
105
|
+
function isPlainObject(value) {
|
|
106
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
107
|
+
const proto = Object.getPrototypeOf(value);
|
|
108
|
+
return proto === Object.prototype || proto === null;
|
|
109
|
+
}
|
|
110
|
+
/** Apply one path op to a detached section, returning the next section. */
|
|
111
|
+
function applyPathOp(section, op) {
|
|
112
|
+
const [head, ...rest] = op.path;
|
|
113
|
+
if (head === void 0) {
|
|
114
|
+
if (op.op === "unset") return {};
|
|
115
|
+
if (!isPlainObject(op.value)) throw new TypeError("settings mutate: setting the section root requires a plain object");
|
|
116
|
+
return { ...op.value };
|
|
117
|
+
}
|
|
118
|
+
if (rest.length === 0) {
|
|
119
|
+
if (op.op === "set") return {
|
|
120
|
+
...section,
|
|
121
|
+
[head]: op.value
|
|
122
|
+
};
|
|
123
|
+
const { [head]: _removed, ...kept } = section;
|
|
124
|
+
return kept;
|
|
125
|
+
}
|
|
126
|
+
const child = section[head];
|
|
127
|
+
if (!isPlainObject(child)) {
|
|
128
|
+
if (op.op === "unset") return section;
|
|
129
|
+
return {
|
|
130
|
+
...section,
|
|
131
|
+
[head]: applyPathOp({}, {
|
|
132
|
+
...op,
|
|
133
|
+
path: rest
|
|
134
|
+
})
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
...section,
|
|
139
|
+
[head]: applyPathOp(child, {
|
|
140
|
+
...op,
|
|
141
|
+
path: rest
|
|
142
|
+
})
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Layer `over` onto `under`: plain objects merge recursively, every other
|
|
147
|
+
* value (arrays included) replaces the lower layer wholesale. `over` never
|
|
148
|
+
* carries `undefined` entries — sections come from parsed documents and write
|
|
149
|
+
* snapshots pass {@link cloneJsonShaped}, which strips them so a sparse patch
|
|
150
|
+
* cannot erase lower keys.
|
|
151
|
+
*/
|
|
152
|
+
function mergeLayers(under, over) {
|
|
153
|
+
if (over === void 0) return under;
|
|
154
|
+
if (!isPlainObject(under) || !isPlainObject(over)) return over;
|
|
155
|
+
const merged = { ...under };
|
|
156
|
+
for (const [key, value] of Object.entries(over)) merged[key] = key in merged ? mergeLayers(merged[key], value) : value;
|
|
157
|
+
return merged;
|
|
158
|
+
}
|
|
159
|
+
/** Recursively freeze one resolved value so handed-out snapshots stay immutable. */
|
|
160
|
+
function deepFreeze(value) {
|
|
161
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
|
|
162
|
+
for (const entry of Object.values(value)) deepFreeze(entry);
|
|
163
|
+
return Object.freeze(value);
|
|
164
|
+
}
|
|
165
|
+
Service.init;
|
|
166
|
+
/**
|
|
167
|
+
* Value mirror of the `FiberState` members {@link isUnloading} compares
|
|
168
|
+
* against: a const enum has no runtime object to import, and the value is
|
|
169
|
+
* needed at runtime (same rationale as the CLI boot driver's mirror).
|
|
170
|
+
*/
|
|
171
|
+
const FIBER_DISPOSED = 4;
|
|
172
|
+
const FIBER_UNLOADING = 5;
|
|
173
|
+
/** Whether the consumer's own fiber is tearing down (not just losing the settings service). */
|
|
174
|
+
function isUnloading(ctx) {
|
|
175
|
+
const state = ctx.fiber.state;
|
|
176
|
+
return state === FIBER_UNLOADING || state === FIBER_DISPOSED;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Install the canonical optional-settings consumer wiring: while a settings
|
|
180
|
+
* service exists, register `ns` with the consumer's composition entry as the
|
|
181
|
+
* `base` layer and point the source thunk at the resolved scope; when the
|
|
182
|
+
* service goes away (disposal, provider reload), fall back to the entry so
|
|
183
|
+
* the consumer keeps working exactly as composed. The registration rides the
|
|
184
|
+
* scoped fiber, so no settings service ever mounted means none of this runs.
|
|
185
|
+
* @param ctx - consumer plugin context owning the wiring.
|
|
186
|
+
* @param ns - the consumer-owned settings namespace.
|
|
187
|
+
* @param schema - schema resolving the namespace (typically the plugin Config).
|
|
188
|
+
* @param entry - the consumer's composition entry config, used as `base`.
|
|
189
|
+
* @param hooks - source sink and change notification.
|
|
190
|
+
*/
|
|
191
|
+
function installSettingsSection(ctx, ns, schema, entry, hooks) {
|
|
192
|
+
ctx.inject(["settings"], (sctx) => {
|
|
193
|
+
const scope = sctx.settings.register(ns, schema, {
|
|
194
|
+
base: entry,
|
|
195
|
+
...hooks.validate === void 0 ? {} : { validate: hooks.validate }
|
|
196
|
+
});
|
|
197
|
+
hooks.setSource(() => scope.get());
|
|
198
|
+
sctx.effect(() => () => {
|
|
199
|
+
if (isUnloading(ctx)) return;
|
|
200
|
+
hooks.setSource(() => entry);
|
|
201
|
+
hooks.onChange();
|
|
202
|
+
});
|
|
203
|
+
hooks.onChange();
|
|
204
|
+
scope.watch(() => {
|
|
205
|
+
if (isUnloading(ctx)) return;
|
|
206
|
+
hooks.onChange();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
14
211
|
//#region src/pairing.ts
|
|
15
212
|
/**
|
|
16
213
|
* Pairing state machine: one active one-time token, a device-session table,
|
|
@@ -803,6 +1000,12 @@ const MOBILE_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
|
803
1000
|
"session.selectModel",
|
|
804
1001
|
"session.rename"
|
|
805
1002
|
]);
|
|
1003
|
+
/**
|
|
1004
|
+
* Locally answered display-preference method (the phone's read-only
|
|
1005
|
+
* surface preferences; never proxied to the host ApiProxy and never a
|
|
1006
|
+
* settings-domain write).
|
|
1007
|
+
*/
|
|
1008
|
+
const MOBILE_PREFERENCES_METHOD = "mobile.preferences";
|
|
806
1009
|
/** One session.list page (thin phones load incrementally). */
|
|
807
1010
|
const SESSION_PAGE_SIZE = 20;
|
|
808
1011
|
/** Encode one list position as an opaque continuation cursor. */
|
|
@@ -836,7 +1039,7 @@ const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`;
|
|
|
836
1039
|
* @returns the routes to register on webServer.
|
|
837
1040
|
*/
|
|
838
1041
|
function makeMobileApiRoutes(deps) {
|
|
839
|
-
const { service, apiProxy } = deps;
|
|
1042
|
+
const { service, apiProxy, mobileEnterToSend } = deps;
|
|
840
1043
|
/** The phone gate: a live paired-device cookie, or nothing else proceeds. */
|
|
841
1044
|
const gateOk = (req) => {
|
|
842
1045
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
@@ -874,7 +1077,8 @@ function makeMobileApiRoutes(deps) {
|
|
|
874
1077
|
return;
|
|
875
1078
|
}
|
|
876
1079
|
const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length);
|
|
877
|
-
|
|
1080
|
+
const local = method === MOBILE_PREFERENCES_METHOD;
|
|
1081
|
+
if (!MOBILE_ALLOWLIST.has(method) && !local) {
|
|
878
1082
|
writeJson(res, 403, {
|
|
879
1083
|
ok: false,
|
|
880
1084
|
error: {
|
|
@@ -909,6 +1113,17 @@ function makeMobileApiRoutes(deps) {
|
|
|
909
1113
|
});
|
|
910
1114
|
return;
|
|
911
1115
|
}
|
|
1116
|
+
if (local) {
|
|
1117
|
+
writeJson(res, 200, {
|
|
1118
|
+
type: "server-response",
|
|
1119
|
+
rpcId,
|
|
1120
|
+
result: {
|
|
1121
|
+
ok: true,
|
|
1122
|
+
value: { mobileEnterToSend: mobileEnterToSend() }
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
912
1127
|
try {
|
|
913
1128
|
const response = await dispatch(apiProxy, method, parsed?.payload, rpcId);
|
|
914
1129
|
writeJson(res, 200, response);
|
|
@@ -1645,6 +1860,7 @@ const Config = z.object({
|
|
|
1645
1860
|
requirePairingForLan: z.boolean().default(true),
|
|
1646
1861
|
publicBaseUrl: z.string(),
|
|
1647
1862
|
autoTunnel: z.boolean().default(false),
|
|
1863
|
+
mobileEnterToSend: z.boolean().default(true),
|
|
1648
1864
|
enabled: z.boolean().default(true)
|
|
1649
1865
|
});
|
|
1650
1866
|
/** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
|
|
@@ -1658,6 +1874,7 @@ const DEFAULTS = {
|
|
|
1658
1874
|
requirePairingForLan: true,
|
|
1659
1875
|
publicBaseUrl: void 0,
|
|
1660
1876
|
autoTunnel: false,
|
|
1877
|
+
mobileEnterToSend: true,
|
|
1661
1878
|
enabled: true
|
|
1662
1879
|
};
|
|
1663
1880
|
/**
|
|
@@ -1674,6 +1891,7 @@ function apply(ctx, config) {
|
|
|
1674
1891
|
requirePairingForLan: config?.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
1675
1892
|
publicBaseUrl: config?.publicBaseUrl,
|
|
1676
1893
|
autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
1894
|
+
mobileEnterToSend: config?.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
|
|
1677
1895
|
enabled: config?.enabled ?? DEFAULTS.enabled
|
|
1678
1896
|
};
|
|
1679
1897
|
let current = () => config ?? {};
|
|
@@ -1687,6 +1905,7 @@ function apply(ctx, config) {
|
|
|
1687
1905
|
requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
1688
1906
|
publicBaseUrl: value.publicBaseUrl,
|
|
1689
1907
|
autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
1908
|
+
mobileEnterToSend: value.mobileEnterToSend ?? DEFAULTS.mobileEnterToSend,
|
|
1690
1909
|
enabled: value.enabled ?? DEFAULTS.enabled
|
|
1691
1910
|
};
|
|
1692
1911
|
};
|
|
@@ -1771,7 +1990,8 @@ function apply(ctx, config) {
|
|
|
1771
1990
|
...makeMobileRoutes(),
|
|
1772
1991
|
...apiProxy !== void 0 ? makeMobileApiRoutes({
|
|
1773
1992
|
service,
|
|
1774
|
-
apiProxy
|
|
1993
|
+
apiProxy,
|
|
1994
|
+
mobileEnterToSend: () => resolve().mobileEnterToSend
|
|
1775
1995
|
}) : [],
|
|
1776
1996
|
...updateRoutes
|
|
1777
1997
|
];
|
package/lib/mobile.js
CHANGED
|
@@ -7570,6 +7570,10 @@ async function listWorkspaces() {
|
|
|
7570
7570
|
const { items } = await callUnary("workspace.list", {});
|
|
7571
7571
|
return items;
|
|
7572
7572
|
}
|
|
7573
|
+
/** Read-only mobile display preferences (answered by the plugin, not the host proxy). */
|
|
7574
|
+
async function fetchMobilePreferences() {
|
|
7575
|
+
return await callUnary("mobile.preferences", {});
|
|
7576
|
+
}
|
|
7573
7577
|
/** One session.list page; omit the cursor for the first page. */
|
|
7574
7578
|
async function listSessions(cursor) {
|
|
7575
7579
|
return await callUnary("session.list", cursor === void 0 ? {} : { cursor });
|
|
@@ -7581,13 +7585,13 @@ async function listSessions(cursor) {
|
|
|
7581
7585
|
async function createSession(options = {}) {
|
|
7582
7586
|
return await callUnary("session.create", options);
|
|
7583
7587
|
}
|
|
7584
|
-
/** One history window; omit beforeSeq for the tail page. */
|
|
7585
|
-
async function history(sessionId, beforeSeq, maxMessages = 30) {
|
|
7588
|
+
/** One history window; omit beforeSeq for the tail page, pass a signal to abort. */
|
|
7589
|
+
async function history(sessionId, beforeSeq, maxMessages = 30, signal) {
|
|
7586
7590
|
return await callUnary("session.history", {
|
|
7587
7591
|
sessionId,
|
|
7588
7592
|
maxMessages,
|
|
7589
7593
|
...beforeSeq !== void 0 ? { beforeSeq } : {}
|
|
7590
|
-
});
|
|
7594
|
+
}, signal);
|
|
7591
7595
|
}
|
|
7592
7596
|
/** Send one text prompt (queued: the agent picks it up in order). */
|
|
7593
7597
|
async function prompt(sessionId, text) {
|
|
@@ -13526,20 +13530,6 @@ function ThemeToggle() {
|
|
|
13526
13530
|
children: dark ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SunIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MoonIcon, {})
|
|
13527
13531
|
});
|
|
13528
13532
|
}
|
|
13529
|
-
//#endregion
|
|
13530
|
-
//#region src/mobile/views/ChatView.tsx
|
|
13531
|
-
/**
|
|
13532
|
-
* Chat level: one session. Loads the history tail page on open, appends
|
|
13533
|
-
* pages upward (loadOlder), folds live mux frames in as they arrive, and
|
|
13534
|
-
* sends prompts through session.prompt.
|
|
13535
|
-
*
|
|
13536
|
-
* Rendering mirrors the desktop web UI's fold discipline on a small screen:
|
|
13537
|
-
* - reasoning text hides behind a collapsed "深度思考" disclosure,
|
|
13538
|
-
* - tool calls behind a collapsed tool disclosure (name + arguments),
|
|
13539
|
-
* - very long assistant text collapses with an explicit expand toggle,
|
|
13540
|
-
* - a toolbar above the composer carries the model (+ thinking effort) and
|
|
13541
|
-
* permission pickers, both as bottom sheets.
|
|
13542
|
-
*/
|
|
13543
13533
|
/** Extract the raw event from one history entry (the fold consumes events only). */
|
|
13544
13534
|
function eventOf(entry) {
|
|
13545
13535
|
return entry.event;
|
|
@@ -13597,26 +13587,74 @@ function ChatView({ session, mux, onBack }) {
|
|
|
13597
13587
|
const [sending, setSending] = (0, import_react.useState)(false);
|
|
13598
13588
|
const scrollRef = (0, import_react.useRef)(void 0);
|
|
13599
13589
|
const pendingRef = (0, import_react.useRef)(false);
|
|
13590
|
+
/**
|
|
13591
|
+
* True while the initial tail page is in flight. Live events arriving in
|
|
13592
|
+
* that window go to {@link liveBufferRef} instead of the message list: the
|
|
13593
|
+
* tail load replaces the list wholesale, so a directly folded event would
|
|
13594
|
+
* flash once, be discarded by the snapshot, and then be skipped forever by
|
|
13595
|
+
* the seq watermark.
|
|
13596
|
+
*/
|
|
13597
|
+
const tailLoadingRef = (0, import_react.useRef)(true);
|
|
13598
|
+
/** Live session events buffered while the initial tail page loads. */
|
|
13599
|
+
const liveBufferRef = (0, import_react.useRef)([]);
|
|
13600
|
+
/** True once the live buffer hit its cap (oldest events were dropped). */
|
|
13601
|
+
const liveBufferOverflowRef = (0, import_react.useRef)(false);
|
|
13600
13602
|
/** The session's permission select (absent = capability not composed). */
|
|
13601
13603
|
const [permissions, setPermissions] = (0, import_react.useState)(void 0);
|
|
13602
13604
|
/** The current model selection for the toolbar chip (best-effort label). */
|
|
13603
13605
|
const [currentModel, setCurrentModel] = (0, import_react.useState)(void 0);
|
|
13604
13606
|
/** Which bottom sheet is open. */
|
|
13605
13607
|
const [sheet, setSheet] = (0, import_react.useState)(null);
|
|
13608
|
+
/**
|
|
13609
|
+
* Composer preference from the plugin's host settings (default true keeps
|
|
13610
|
+
* the legacy Enter-to-send behavior until the preference loads).
|
|
13611
|
+
*/
|
|
13612
|
+
const [mobileEnterToSend, setMobileEnterToSend] = (0, import_react.useState)(true);
|
|
13613
|
+
(0, import_react.useEffect)(() => {
|
|
13614
|
+
let cancelled = false;
|
|
13615
|
+
fetchMobilePreferences().then((preferences) => {
|
|
13616
|
+
if (!cancelled) setMobileEnterToSend(preferences.mobileEnterToSend !== false);
|
|
13617
|
+
}, () => {});
|
|
13618
|
+
return () => {
|
|
13619
|
+
cancelled = true;
|
|
13620
|
+
};
|
|
13621
|
+
}, []);
|
|
13606
13622
|
(0, import_react.useEffect)(() => {
|
|
13607
13623
|
let cancelled = false;
|
|
13624
|
+
const controller = new AbortController();
|
|
13625
|
+
const timeout = setTimeout(() => {
|
|
13626
|
+
controller.abort(new DOMException("history load timed out", "TimeoutError"));
|
|
13627
|
+
}, 15e3);
|
|
13628
|
+
tailLoadingRef.current = true;
|
|
13629
|
+
liveBufferRef.current = [];
|
|
13630
|
+
liveBufferOverflowRef.current = false;
|
|
13608
13631
|
setLoading(true);
|
|
13609
13632
|
setError(void 0);
|
|
13610
13633
|
setMessages([]);
|
|
13611
|
-
loadHistory(session.sessionId).then((page) => {
|
|
13634
|
+
loadHistory(session.sessionId, void 0, controller.signal).then((page) => {
|
|
13612
13635
|
if (cancelled) return;
|
|
13613
|
-
|
|
13636
|
+
const buffered = liveBufferRef.current;
|
|
13637
|
+
liveBufferRef.current = [];
|
|
13638
|
+
tailLoadingRef.current = false;
|
|
13639
|
+
setMessages(foldEvents(buffered, foldEvents(page.events.map(eventOf))));
|
|
13614
13640
|
setHasOlder(page.hasMore);
|
|
13615
13641
|
setLoading(false);
|
|
13616
13642
|
const projections = page.projections?.values;
|
|
13617
13643
|
setPermissions(parsePermissionSelect(projections?.["permissions"]));
|
|
13644
|
+
if (liveBufferOverflowRef.current) loadHistory(session.sessionId, void 0, controller.signal).then((fresh) => {
|
|
13645
|
+
if (cancelled) return;
|
|
13646
|
+
setMessages((previous) => foldEvents(fresh.events.map(eventOf), previous));
|
|
13647
|
+
liveBufferOverflowRef.current = false;
|
|
13648
|
+
}, (reason) => {
|
|
13649
|
+
if (cancelled) return;
|
|
13650
|
+
console.warn("history tail re-pull after live buffer overflow failed", reason);
|
|
13651
|
+
});
|
|
13618
13652
|
}, (reason) => {
|
|
13619
13653
|
if (cancelled) return;
|
|
13654
|
+
const buffered = liveBufferRef.current;
|
|
13655
|
+
liveBufferRef.current = [];
|
|
13656
|
+
tailLoadingRef.current = false;
|
|
13657
|
+
if (buffered.length > 0) setMessages(foldEvents(buffered));
|
|
13620
13658
|
setError(errorText(reason));
|
|
13621
13659
|
setLoading(false);
|
|
13622
13660
|
});
|
|
@@ -13625,6 +13663,8 @@ function ChatView({ session, mux, onBack }) {
|
|
|
13625
13663
|
}, () => {});
|
|
13626
13664
|
return () => {
|
|
13627
13665
|
cancelled = true;
|
|
13666
|
+
clearTimeout(timeout);
|
|
13667
|
+
controller.abort();
|
|
13628
13668
|
};
|
|
13629
13669
|
}, [session.sessionId]);
|
|
13630
13670
|
(0, import_react.useEffect)(() => {
|
|
@@ -13632,7 +13672,19 @@ function ChatView({ session, mux, onBack }) {
|
|
|
13632
13672
|
return mux.onFrame((frame) => {
|
|
13633
13673
|
if (frame.type === "session/event") {
|
|
13634
13674
|
if (frame.sessionId !== session.sessionId) return;
|
|
13635
|
-
|
|
13675
|
+
const event = frame.event;
|
|
13676
|
+
if (tailLoadingRef.current) {
|
|
13677
|
+
if (liveBufferRef.current.length >= 500) {
|
|
13678
|
+
liveBufferRef.current.shift();
|
|
13679
|
+
if (!liveBufferOverflowRef.current) {
|
|
13680
|
+
console.warn(`history tail is slow: live buffer reached 500 events; oldest buffered events will be re-fetched`);
|
|
13681
|
+
liveBufferOverflowRef.current = true;
|
|
13682
|
+
}
|
|
13683
|
+
}
|
|
13684
|
+
liveBufferRef.current.push(event);
|
|
13685
|
+
return;
|
|
13686
|
+
}
|
|
13687
|
+
setMessages((previous) => foldEvents([event], previous));
|
|
13636
13688
|
return;
|
|
13637
13689
|
}
|
|
13638
13690
|
if (frame.type === "session/projection" && frame.sessionId === session.sessionId && frame.key === "permissions") setPermissions(parsePermissionSelect(frame.value));
|
|
@@ -13800,13 +13852,13 @@ function ChatView({ session, mux, onBack }) {
|
|
|
13800
13852
|
className: "chat-input",
|
|
13801
13853
|
rows: 1,
|
|
13802
13854
|
value: input,
|
|
13803
|
-
placeholder: "输入消息,Enter 发送…",
|
|
13804
|
-
enterKeyHint: "send",
|
|
13855
|
+
placeholder: mobileEnterToSend ? "输入消息,Enter 发送…" : "输入消息,Enter 换行,点按钮发送…",
|
|
13856
|
+
enterKeyHint: mobileEnterToSend ? "send" : "enter",
|
|
13805
13857
|
onChange: (event) => {
|
|
13806
13858
|
setInput(event.target.value);
|
|
13807
13859
|
},
|
|
13808
13860
|
onKeyDown: (event) => {
|
|
13809
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
13861
|
+
if (mobileEnterToSend && event.key === "Enter" && !event.shiftKey) {
|
|
13810
13862
|
event.preventDefault();
|
|
13811
13863
|
send();
|
|
13812
13864
|
}
|
|
@@ -14503,13 +14555,18 @@ function SessionListView({ workspace, onBack, onPick }) {
|
|
|
14503
14555
|
* @param props - the pick action.
|
|
14504
14556
|
* @returns the roster.
|
|
14505
14557
|
*/
|
|
14506
|
-
function WorkspaceView({ onPick }) {
|
|
14558
|
+
function WorkspaceView({ initialWorkspaceId, onPick }) {
|
|
14507
14559
|
const [items, setItems] = (0, import_react.useState)(void 0);
|
|
14508
14560
|
const [error, setError] = (0, import_react.useState)(void 0);
|
|
14509
14561
|
(0, import_react.useEffect)(() => {
|
|
14510
14562
|
let cancelled = false;
|
|
14511
14563
|
listWorkspaces().then((rows) => {
|
|
14512
14564
|
if (cancelled) return;
|
|
14565
|
+
const target = initialWorkspaceId === void 0 ? void 0 : rows.find((workspace) => workspace.workspaceId === initialWorkspaceId);
|
|
14566
|
+
if (target !== void 0) {
|
|
14567
|
+
onPick(target);
|
|
14568
|
+
return;
|
|
14569
|
+
}
|
|
14513
14570
|
setItems(rows);
|
|
14514
14571
|
}, (reason) => {
|
|
14515
14572
|
if (cancelled) return;
|
|
@@ -14518,7 +14575,7 @@ function WorkspaceView({ onPick }) {
|
|
|
14518
14575
|
return () => {
|
|
14519
14576
|
cancelled = true;
|
|
14520
14577
|
};
|
|
14521
|
-
}, []);
|
|
14578
|
+
}, [initialWorkspaceId, onPick]);
|
|
14522
14579
|
if (error !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
14523
14580
|
className: "mobile",
|
|
14524
14581
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
|
|
@@ -14617,6 +14674,11 @@ function WorkspaceView({ onPick }) {
|
|
|
14617
14674
|
* router, no state library: the surface is three fixed levels with a back
|
|
14618
14675
|
* affordance, and every piece of data is fetched on demand.
|
|
14619
14676
|
*/
|
|
14677
|
+
/** Read the optional workspace target carried from the pairing QR flow. */
|
|
14678
|
+
function mobileWorkspaceTarget(search) {
|
|
14679
|
+
const value = new URLSearchParams(search).get("workspace");
|
|
14680
|
+
return value === null || value === "" ? void 0 : value;
|
|
14681
|
+
}
|
|
14620
14682
|
/** Map a list row to the surface model; the title comes from projections when present. */
|
|
14621
14683
|
function toSessionView(item) {
|
|
14622
14684
|
const titleValue = item.projections?.values?.title;
|
|
@@ -14647,6 +14709,7 @@ function formatTime(epochMs) {
|
|
|
14647
14709
|
*/
|
|
14648
14710
|
function App() {
|
|
14649
14711
|
const [route, setRoute] = (0, import_react.useState)({ kind: "workspaces" });
|
|
14712
|
+
const [initialWorkspaceId, setInitialWorkspaceId] = (0, import_react.useState)(() => mobileWorkspaceTarget(window.location.search));
|
|
14650
14713
|
const muxRef = (0, import_react.useRef)(void 0);
|
|
14651
14714
|
(0, import_react.useEffect)(() => {
|
|
14652
14715
|
const mux = new MuxClient();
|
|
@@ -14676,14 +14739,24 @@ function App() {
|
|
|
14676
14739
|
workspace
|
|
14677
14740
|
});
|
|
14678
14741
|
}, []);
|
|
14742
|
+
const openWorkspace = (0, import_react.useCallback)((workspace) => {
|
|
14743
|
+
if (initialWorkspaceId !== void 0) {
|
|
14744
|
+
const url = new URL(window.location.href);
|
|
14745
|
+
url.searchParams.delete("workspace");
|
|
14746
|
+
window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
|
|
14747
|
+
setInitialWorkspaceId(void 0);
|
|
14748
|
+
}
|
|
14749
|
+
setRoute({
|
|
14750
|
+
kind: "sessions",
|
|
14751
|
+
workspace
|
|
14752
|
+
});
|
|
14753
|
+
}, [initialWorkspaceId]);
|
|
14679
14754
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
14680
14755
|
className: "mobile",
|
|
14681
|
-
children: route.kind === "workspaces" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceView, {
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
});
|
|
14686
|
-
} }) : route.kind === "sessions" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SessionListView, {
|
|
14756
|
+
children: route.kind === "workspaces" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceView, {
|
|
14757
|
+
initialWorkspaceId,
|
|
14758
|
+
onPick: openWorkspace
|
|
14759
|
+
}) : route.kind === "sessions" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SessionListView, {
|
|
14687
14760
|
workspace: route.workspace,
|
|
14688
14761
|
onBack: back,
|
|
14689
14762
|
onPick: (session) => {
|
|
@@ -14714,8 +14787,8 @@ function staleHostHint(message) {
|
|
|
14714
14787
|
return /^HTTP 403/.test(message) ? "宿主端插件可能仍在运行旧版本:请重启 dsh web 后再试。" : void 0;
|
|
14715
14788
|
}
|
|
14716
14789
|
/** Fetch one history page (tail by default) — thin wrapper so views share the call shape. */
|
|
14717
|
-
function loadHistory(sessionId, beforeSeq) {
|
|
14718
|
-
return history(sessionId, beforeSeq);
|
|
14790
|
+
function loadHistory(sessionId, beforeSeq, signal) {
|
|
14791
|
+
return history(sessionId, beforeSeq, void 0, signal);
|
|
14719
14792
|
}
|
|
14720
14793
|
//#endregion
|
|
14721
14794
|
//#region src/mobile/mobile-styles.ts
|
|
@@ -15486,6 +15559,9 @@ body {
|
|
|
15486
15559
|
min-width: 0;
|
|
15487
15560
|
min-height: 40px;
|
|
15488
15561
|
max-height: 120px;
|
|
15562
|
+
/* Grow with the content (still bounded by min/max-height); browsers
|
|
15563
|
+
without field-sizing support ignore this line and keep the old size. */
|
|
15564
|
+
field-sizing: content;
|
|
15489
15565
|
padding: 9px 12px;
|
|
15490
15566
|
border: 1px solid var(--m-border);
|
|
15491
15567
|
border-radius: 10px;
|