@linxin666/dsh-remote-web-ui 0.1.18 → 0.1.20
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/lib/client.js +54 -11
- package/lib/client.js.map +1 -1
- package/lib/index.js +137 -96
- package/lib/mobile.js +18 -5
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +41 -11
- package/lib/types/client/RemotePanel.d.ts +1 -0
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +5 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +9 -2
- package/lib/types/client/UpdateEntry.js +1 -1
- package/lib/types/client/UpdatePanel.js +1 -1
- package/lib/types/client/deep-link.js +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +20 -14
- package/lib/types/client/settings-form.d.ts +8 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +15 -1
- package/lib/types/client/update-api.d.ts.map +1 -1
- package/lib/types/gate.d.ts +4 -9
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +5 -32
- package/lib/types/http.d.ts +14 -0
- package/lib/types/http.d.ts.map +1 -0
- package/lib/types/http.js +27 -0
- package/lib/types/index.d.ts +3 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +12 -10
- package/lib/types/invariant.d.ts.map +1 -1
- package/lib/types/loopback.d.ts +25 -0
- package/lib/types/loopback.d.ts.map +1 -0
- package/lib/types/loopback.js +68 -0
- package/lib/types/mobile/api.js +1 -1
- package/lib/types/mobile/index.js +3 -3
- package/lib/types/mobile/markdown.d.ts +6 -4
- package/lib/types/mobile/markdown.d.ts.map +1 -1
- package/lib/types/mobile/markdown.js +8 -4
- package/lib/types/mobile/markdown.test.js +5 -1
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +14 -1
- package/lib/types/mobile/messages.test.js +1 -1
- package/lib/types/mobile/mobile-theme.test.js +2 -2
- package/lib/types/mobile/mux.d.ts.map +1 -1
- package/lib/types/mobile/mux.js +1 -1
- package/lib/types/mobile/mux.test.js +1 -1
- package/lib/types/mobile/rpc.d.ts.map +1 -1
- package/lib/types/mobile/theme-toggle.js +1 -1
- package/lib/types/mobile/views/App.d.ts +0 -11
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +6 -6
- package/lib/types/mobile/views/ChatView.js +6 -6
- package/lib/types/mobile/views/ChatView.test.js +3 -3
- package/lib/types/mobile/views/SessionListView.js +3 -3
- package/lib/types/mobile/views/SessionListView.test.js +2 -2
- package/lib/types/mobile/views/WorkspaceView.js +3 -3
- package/lib/types/mobile/views/WorkspaceView.test.js +3 -3
- package/lib/types/mobile-api.d.ts +1 -1
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +11 -18
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +17 -11
- package/lib/types/mount-once.d.ts +25 -0
- package/lib/types/mount-once.d.ts.map +1 -0
- package/lib/types/mount-once.js +42 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/routes.d.ts +6 -6
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +14 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/update-routes.d.ts +2 -2
- package/lib/types/update-routes.d.ts.map +1 -1
- package/lib/types/update-routes.js +1 -6
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +6 -3
- package/package.json +1 -1
- package/src/client/RemoteEntry.tsx +38 -6
- package/src/client/RemotePanel.tsx +1 -1
- package/src/client/RemoteSettingsCard.tsx +8 -0
- package/src/client/index.ts +13 -7
- package/src/client/settings-form.ts +15 -1
- package/src/gate.ts +5 -30
- package/src/http.ts +30 -0
- package/src/index.ts +4 -1
- package/src/loopback.ts +63 -0
- package/src/mobile/markdown.test.ts +5 -0
- package/src/mobile/markdown.ts +7 -4
- package/src/mobile/messages.ts +14 -1
- package/src/mobile/views/App.tsx +0 -12
- package/src/mobile-api.ts +9 -17
- package/src/mobile-routes.ts +16 -10
- package/src/mount-once.ts +48 -0
- package/src/routes.ts +11 -17
- package/src/update-routes.ts +1 -7
- package/src/update.ts +6 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/loopback.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Loopback trust fence shared by the host route families: socket address,
|
|
4
|
+
* Host header, and browser same-origin markers. Packages receive this file as
|
|
5
|
+
* a generated copy via scripts/sync-shared.mjs; edit the shared source and
|
|
6
|
+
* re-run the sync instead of editing a copy.
|
|
7
|
+
*
|
|
8
|
+
* Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
|
|
9
|
+
* remote-web-ui gate), localhost hostnames, plus the browser same-origin
|
|
10
|
+
* markers (sec-fetch-site and Origin) for the request-level fence.
|
|
11
|
+
* @module dsh-web-ui-shared/host/loopback
|
|
12
|
+
*/
|
|
13
|
+
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
14
|
+
export function isIPv4Loopback(v4) {
|
|
15
|
+
const parts = v4.split('.');
|
|
16
|
+
return parts.length === 4
|
|
17
|
+
&& parts[0] === '127'
|
|
18
|
+
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
19
|
+
}
|
|
20
|
+
/** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
|
|
21
|
+
export function isLoopbackAddress(address) {
|
|
22
|
+
if (address === undefined)
|
|
23
|
+
return false;
|
|
24
|
+
const normalized = address.toLowerCase();
|
|
25
|
+
if (normalized === '::1')
|
|
26
|
+
return true;
|
|
27
|
+
if (normalized.startsWith('::ffff:'))
|
|
28
|
+
return isIPv4Loopback(normalized.slice('::ffff:'.length));
|
|
29
|
+
return isIPv4Loopback(normalized);
|
|
30
|
+
}
|
|
31
|
+
/** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
|
|
32
|
+
export function isLoopbackHostname(hostname) {
|
|
33
|
+
if (hostname === 'localhost' || hostname === '[::1]')
|
|
34
|
+
return true;
|
|
35
|
+
return isIPv4Loopback(hostname);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Request-level trust fence: a loopback socket address AND a loopback Host
|
|
39
|
+
* header, plus browser same-origin markers. The socket address is
|
|
40
|
+
* authoritative; X-Forwarded-For is never trusted.
|
|
41
|
+
*/
|
|
42
|
+
export function isLoopbackRequest(request) {
|
|
43
|
+
if (!isLoopbackAddress(request.socket.remoteAddress))
|
|
44
|
+
return false;
|
|
45
|
+
const host = request.headers.host;
|
|
46
|
+
if (typeof host !== 'string')
|
|
47
|
+
return false;
|
|
48
|
+
let hostUrl;
|
|
49
|
+
try {
|
|
50
|
+
hostUrl = new URL('http://' + host);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (!isLoopbackHostname(hostUrl.hostname))
|
|
56
|
+
return false;
|
|
57
|
+
if (request.headers['sec-fetch-site'] === 'cross-site')
|
|
58
|
+
return false;
|
|
59
|
+
const origin = request.headers.origin;
|
|
60
|
+
if (origin === undefined)
|
|
61
|
+
return true;
|
|
62
|
+
try {
|
|
63
|
+
return new URL(origin).host === hostUrl.host;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
package/lib/types/mobile/api.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (type-only imports; the wire schemas stay in the bundle only through the
|
|
5
5
|
* rpc/mux layers).
|
|
6
6
|
*/
|
|
7
|
-
import { callUnary } from
|
|
7
|
+
import { callUnary } from './rpc.js';
|
|
8
8
|
/** The workspace roster (session ids come back per workspace). */
|
|
9
9
|
export async function listWorkspaces() {
|
|
10
10
|
const { items } = await callUnary('workspace.list', {});
|
|
@@ -8,9 +8,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
* chat (history paged on demand + live mux stream + prompt input).
|
|
9
9
|
*/
|
|
10
10
|
import { createRoot } from 'react-dom/client';
|
|
11
|
-
import { App } from
|
|
12
|
-
import { mobileCss } from
|
|
13
|
-
import { initMobileTheme } from
|
|
11
|
+
import { App } from './views/App.js';
|
|
12
|
+
import { mobileCss } from './mobile-styles.js';
|
|
13
|
+
import { initMobileTheme } from './mobile-theme.js';
|
|
14
14
|
// Apply the persisted (or default light) theme before first paint, so the
|
|
15
15
|
// page never flashes the wrong palette.
|
|
16
16
|
initMobileTheme();
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
export declare function escapeHtml(text: string): string;
|
|
14
14
|
/**
|
|
15
15
|
* Guard a raw link/image target against dangerous protocols. Returns the
|
|
16
|
-
* (trimmed) raw string when safe, else null. Only http:, https:, mailto
|
|
17
|
-
*
|
|
18
|
-
* — javascript:, data:, vbscript:, etc. —
|
|
19
|
-
*
|
|
16
|
+
* (trimmed) raw string when safe, else null. Only http:, https:, mailto:,
|
|
17
|
+
* fragment anchors (#...) and strictly relative paths are allowed; anything
|
|
18
|
+
* with another scheme — javascript:, data:, vbscript:, etc. — or a
|
|
19
|
+
* protocol-relative //host target (the browser resolves it against the
|
|
20
|
+
* current scheme, reaching an arbitrary origin) is rejected so the value
|
|
21
|
+
* never reaches dangerouslySetInnerHTML.
|
|
20
22
|
*/
|
|
21
23
|
export declare function safeUrl(raw: string): string | null;
|
|
22
24
|
/** Inline pass: code spans, bold, italic, strikethrough, images, links. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../src/mobile/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,sCAAsC;AACtC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO/C;AAED
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../src/mobile/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,sCAAsC;AACtC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASlD;AAgBD,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqFjD;AAED,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CA+HrD"}
|
|
@@ -20,10 +20,12 @@ export function escapeHtml(text) {
|
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* Guard a raw link/image target against dangerous protocols. Returns the
|
|
23
|
-
* (trimmed) raw string when safe, else null. Only http:, https:, mailto
|
|
24
|
-
*
|
|
25
|
-
* — javascript:, data:, vbscript:, etc. —
|
|
26
|
-
*
|
|
23
|
+
* (trimmed) raw string when safe, else null. Only http:, https:, mailto:,
|
|
24
|
+
* fragment anchors (#...) and strictly relative paths are allowed; anything
|
|
25
|
+
* with another scheme — javascript:, data:, vbscript:, etc. — or a
|
|
26
|
+
* protocol-relative //host target (the browser resolves it against the
|
|
27
|
+
* current scheme, reaching an arbitrary origin) is rejected so the value
|
|
28
|
+
* never reaches dangerouslySetInnerHTML.
|
|
27
29
|
*/
|
|
28
30
|
export function safeUrl(raw) {
|
|
29
31
|
const trimmed = raw.trim();
|
|
@@ -31,6 +33,8 @@ export function safeUrl(raw) {
|
|
|
31
33
|
return null;
|
|
32
34
|
if (trimmed.startsWith('#'))
|
|
33
35
|
return trimmed;
|
|
36
|
+
if (trimmed.startsWith('//'))
|
|
37
|
+
return null;
|
|
34
38
|
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed);
|
|
35
39
|
if (scheme === null)
|
|
36
40
|
return trimmed;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* stripped, and code fences never parse their content.
|
|
5
5
|
*/
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
|
-
import { escapeHtml, renderInline, renderMarkdown, safeUrl } from
|
|
7
|
+
import { escapeHtml, renderInline, renderMarkdown, safeUrl } from './markdown.js';
|
|
8
8
|
describe('safeUrl', () => {
|
|
9
9
|
it('allows http/https/mailto/fragments and scheme-less targets', () => {
|
|
10
10
|
expect(safeUrl('https://example.com/a')).toBe('https://example.com/a');
|
|
@@ -20,6 +20,10 @@ describe('safeUrl', () => {
|
|
|
20
20
|
expect(safeUrl('')).toBeNull();
|
|
21
21
|
expect(safeUrl(' ')).toBeNull();
|
|
22
22
|
});
|
|
23
|
+
it('rejects protocol-relative targets that escape the origin', () => {
|
|
24
|
+
expect(safeUrl('//attacker.example/x')).toBeNull();
|
|
25
|
+
expect(safeUrl(' //attacker.example/track.png')).toBeNull();
|
|
26
|
+
});
|
|
23
27
|
});
|
|
24
28
|
describe('renderInline', () => {
|
|
25
29
|
it('renders code, bold, italic, strikethrough and links', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../../src/mobile/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAA;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;IACD,sFAAsF;IACtF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAC1B;AA6eD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../../src/mobile/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAA;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;IACD,sFAAsF;IACtF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAC1B;AA6eD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,EAAE,CAsB7G"}
|
|
@@ -497,5 +497,18 @@ export function foldEvents(events, existing) {
|
|
|
497
497
|
continue;
|
|
498
498
|
applyEvent(state, event);
|
|
499
499
|
}
|
|
500
|
-
|
|
500
|
+
// Incremental tails (the live-stream hot path) are appended in seq order
|
|
501
|
+
// or updated in place, so the list is usually ordered already — verify in
|
|
502
|
+
// O(n) and skip the O(n log n) re-sort when it is.
|
|
503
|
+
const out = state.messages;
|
|
504
|
+
let ordered = true;
|
|
505
|
+
for (let index = 1; index < out.length; index += 1) {
|
|
506
|
+
const prev = out[index - 1];
|
|
507
|
+
const current = out[index];
|
|
508
|
+
if (prev.seq > current.seq || (prev.seq === current.seq && prev.id >= current.id)) {
|
|
509
|
+
ordered = false;
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return ordered ? out : [...out].sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
|
|
501
514
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** foldEvents: message-list folding from a session event stream. */
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { foldEvents } from
|
|
3
|
+
import { foldEvents } from './messages.js';
|
|
4
4
|
/** Assemble one event with an auto-incrementing seq / time. */
|
|
5
5
|
function makeEvent(type, data, seq, time = seq * 1_000) {
|
|
6
6
|
return { type, seq, time, data };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
/** mobile-theme: light default, persisted dark toggle, document wiring. */
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { getMobileTheme, setMobileTheme, subscribeMobileTheme, toggleMobileTheme, } from
|
|
4
|
+
import { getMobileTheme, setMobileTheme, subscribeMobileTheme, toggleMobileTheme, } from './mobile-theme.js';
|
|
5
5
|
const STORAGE_KEY = 'dsh.remote.theme';
|
|
6
6
|
/** jsdom in this setup ships a bare localStorage object; install a real fake. */
|
|
7
7
|
function makeStorage() {
|
|
@@ -54,7 +54,7 @@ describe('mobile-theme', () => {
|
|
|
54
54
|
it('boot reads the stored theme and initMobileTheme applies it', async () => {
|
|
55
55
|
storage.setItem(STORAGE_KEY, 'dark');
|
|
56
56
|
vi.resetModules();
|
|
57
|
-
const fresh = await import(
|
|
57
|
+
const fresh = await import('./mobile-theme.js');
|
|
58
58
|
expect(fresh.getMobileTheme()).toBe('dark');
|
|
59
59
|
fresh.initMobileTheme();
|
|
60
60
|
expect(document.documentElement.dataset.theme).toBe('dark');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACrD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1C,KAAK,IAAI,IAAI,CAAA;CACd;AAuBD;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;IACxE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,wEAAwE;IACxE,OAAO,CAAC,gBAAgB,CAAoB;IAC5C,6EAA6E;IAC7E,OAAO,CAAC,UAAU,CAAI;IACtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAQ;IACxB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,+FAA+F;IAC/F,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAQ;IACvB,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAI;IAEtB;;;OAGG;
|
|
1
|
+
{"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACrD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1C,KAAK,IAAI,IAAI,CAAA;CACd;AAuBD;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;IACxE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,wEAAwE;IACxE,OAAO,CAAC,gBAAgB,CAAoB;IAC5C,6EAA6E;IAC7E,OAAO,CAAC,UAAU,CAAI;IACtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAQ;IACxB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,+FAA+F;IAC/F,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAQ;IACvB,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAI;IAEtB;;;OAGG;IACH,YAAY,GAAG,SAAsB,EAAE,OAAO,GAAE,gBAAqB,EAOpE;IAED,+EAA+E;IAC/E,KAAK,IAAI,IAAI,CAKZ;IAED,sBAAsB;IACtB,IAAI,IAAI,IAAI,CAOX;IAED,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI,CAGvD;IAED;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAU3C;IAED,OAAO,CAAC,OAAO;IAqBf;;;;;OAKG;IACH,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,IAAI;IAkBZ,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;IAInB;;;;OAIG;YACW,QAAQ;IAsBtB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,IAAI;IAUZ,OAAO,CAAC,WAAW;CAapB"}
|
package/lib/types/mobile/mux.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { muxFrameSchema } from '@deepseek-ai/dsh-host-apiproxy/api/events.schema';
|
|
27
27
|
import { serverRequestSchema } from '@deepseek-ai/dsh-host-apiproxy/api/rpc.schema';
|
|
28
|
-
import { history as fetchHistory } from
|
|
28
|
+
import { history as fetchHistory } from './api.js';
|
|
29
29
|
/** Browser default source factory. */
|
|
30
30
|
function browserSource(url) {
|
|
31
31
|
// The DOM EventSource is structurally compatible; the `this`-typed handler
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** mux: live-event client, SSE delivery + stall-driven polling fallback. */
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { MuxClient } from
|
|
3
|
+
import { MuxClient } from './mux.js';
|
|
4
4
|
/** Create an EventSource factory that records every opened source. */
|
|
5
5
|
function makeSources() {
|
|
6
6
|
const sources = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../../src/mobile/rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,0EAA0E;AAC1E,qBAAa,iBAAkB,SAAQ,KAAK;
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../../src/mobile/rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,0EAA0E;AAC1E,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED,kEAAkE;AAClE,qBAAa,YAAa,SAAQ,KAAK;IACrC,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IAEjD,YAAY,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAInD;CACF;AAID,8EAA8E;AAC9E,wBAAgB,SAAS,IAAI,MAAM,CAMlC;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,CAAC,CAAC,CAmCZ"}
|
|
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* mode); the choice persists across visits.
|
|
7
7
|
*/
|
|
8
8
|
import { useSyncExternalStore } from 'react';
|
|
9
|
-
import { getMobileTheme, subscribeMobileTheme, toggleMobileTheme } from
|
|
9
|
+
import { getMobileTheme, subscribeMobileTheme, toggleMobileTheme } from './mobile-theme.js';
|
|
10
10
|
/** Sun glyph: circle + eight rays (inline SVG, no emoji). */
|
|
11
11
|
function SunIcon() {
|
|
12
12
|
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": true, children: [_jsx("circle", { cx: "12", cy: "12", r: "4.2" }), _jsx("path", { d: "M12 2.5v2.4M12 19.1v2.4M2.5 12h2.4M19.1 12h2.4M5 5l1.7 1.7M17.3 17.3 19 19M19 5l-1.7 1.7M6.7 17.3 5 19" })] }));
|
|
@@ -14,17 +14,6 @@ export interface SessionView {
|
|
|
14
14
|
running: boolean;
|
|
15
15
|
blank: boolean;
|
|
16
16
|
}
|
|
17
|
-
/** One rendered chat message (produced by the fold). */
|
|
18
|
-
export interface RenderMessage {
|
|
19
|
-
id: string;
|
|
20
|
-
kind: 'user' | 'assistant';
|
|
21
|
-
text: string;
|
|
22
|
-
seq: number;
|
|
23
|
-
time: number;
|
|
24
|
-
pending?: boolean;
|
|
25
|
-
failed?: boolean;
|
|
26
|
-
toolSummary?: string;
|
|
27
|
-
}
|
|
28
17
|
/** Read the optional workspace target carried from the pairing QR flow. */
|
|
29
18
|
export declare function mobileWorkspaceTarget(search: string): string | undefined;
|
|
30
19
|
/** Map a list row to the surface model; the title comes from projections when present. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAA2B,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAazF,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,
|
|
1
|
+
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAA2B,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAazF,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;CACnD,GAAG,WAAW,CAad;AAED,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASlD;AAED;;;GAGG;AACH,wBAAgB,GAAG,gCA4DlB;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKhD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIjE;AAED,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,4CAEtF;AAED,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,CAAA"}
|
|
@@ -6,12 +6,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
6
6
|
* affordance, and every piece of data is fetched on demand.
|
|
7
7
|
*/
|
|
8
8
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
|
-
import { history as fetchHistory, listSessions, listWorkspaces, prompt } from
|
|
10
|
-
import { MuxClient } from
|
|
11
|
-
import { RpcCallError, RpcTransportError } from
|
|
12
|
-
import { ChatView } from
|
|
13
|
-
import { SessionListView } from
|
|
14
|
-
import { WorkspaceView as WorkspaceRoster } from
|
|
9
|
+
import { history as fetchHistory, listSessions, listWorkspaces, prompt } from '../api.js';
|
|
10
|
+
import { MuxClient } from '../mux.js';
|
|
11
|
+
import { RpcCallError, RpcTransportError } from '../rpc.js';
|
|
12
|
+
import { ChatView } from './ChatView.js';
|
|
13
|
+
import { SessionListView } from './SessionListView.js';
|
|
14
|
+
import { WorkspaceView as WorkspaceRoster } from './WorkspaceView.js';
|
|
15
15
|
/** Read the optional workspace target carried from the pairing QR flow. */
|
|
16
16
|
export function mobileWorkspaceTarget(search) {
|
|
17
17
|
const value = new URLSearchParams(search).get('workspace');
|
|
@@ -12,12 +12,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* permission pickers, both as bottom sheets.
|
|
13
13
|
*/
|
|
14
14
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
|
-
import { loadHistory, prompt } from
|
|
16
|
-
import { errorText, formatTime, staleHostHint } from
|
|
17
|
-
import { fetchMobilePreferences, models, selectModel, sendCommand } from
|
|
18
|
-
import { foldEvents } from
|
|
19
|
-
import { renderMarkdown } from
|
|
20
|
-
import { ThemeToggle } from
|
|
15
|
+
import { loadHistory, prompt } from './App.js';
|
|
16
|
+
import { errorText, formatTime, staleHostHint } from './App.js';
|
|
17
|
+
import { fetchMobilePreferences, models, selectModel, sendCommand } from '../api.js';
|
|
18
|
+
import { foldEvents } from '../messages.js';
|
|
19
|
+
import { renderMarkdown } from '../markdown.js';
|
|
20
|
+
import { ThemeToggle } from '../theme-toggle.js';
|
|
21
21
|
/**
|
|
22
22
|
* Hard cap on live events buffered while the initial history tail page is in
|
|
23
23
|
* flight. Beyond this the oldest buffered event is dropped and a follow-up
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
/** ChatView: collapsible message folds, toolbar chips, and the bottom sheets. */
|
|
4
4
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
-
import { ChatView, MAX_TAIL_BUFFER_EVENTS } from
|
|
6
|
+
import { ChatView, MAX_TAIL_BUFFER_EVENTS } from './ChatView.js';
|
|
7
7
|
// The api module is fully mocked; App.tsx's history wrapper is overridden to
|
|
8
8
|
// feed fixed history pages, its pure helpers (errorText / formatTime) stay real.
|
|
9
9
|
vi.mock('../api.ts', () => ({
|
|
@@ -20,8 +20,8 @@ vi.mock('./App.tsx', async (importOriginal) => {
|
|
|
20
20
|
prompt: vi.fn(async () => { }),
|
|
21
21
|
};
|
|
22
22
|
});
|
|
23
|
-
import { fetchMobilePreferences, models, selectModel, sendCommand } from
|
|
24
|
-
import { loadHistory, prompt } from
|
|
23
|
+
import { fetchMobilePreferences, models, selectModel, sendCommand } from '../api.js';
|
|
24
|
+
import { loadHistory, prompt } from './App.js';
|
|
25
25
|
const session = {
|
|
26
26
|
sessionId: 's-1',
|
|
27
27
|
title: '测试会话',
|
|
@@ -12,9 +12,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* the new chat — the same "new session opens" flow as the desktop UI.
|
|
13
13
|
*/
|
|
14
14
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
15
|
-
import { createSession, listSessions, listWorkspaces } from
|
|
16
|
-
import { errorText, formatTime, staleHostHint, toSessionView } from
|
|
17
|
-
import { ThemeToggle } from
|
|
15
|
+
import { createSession, listSessions, listWorkspaces } from '../api.js';
|
|
16
|
+
import { errorText, formatTime, staleHostHint, toSessionView } from './App.js';
|
|
17
|
+
import { ThemeToggle } from '../theme-toggle.js';
|
|
18
18
|
/** Rows that belong to the opened workspace (its owned session id set). */
|
|
19
19
|
function ownedItems(page, workspace) {
|
|
20
20
|
const owned = new Set(workspace.sessionIds);
|
|
@@ -3,14 +3,14 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
/** SessionListView: owned-row filtering, incremental pages, session creation. */
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
-
import { SessionListView } from
|
|
6
|
+
import { SessionListView } from './SessionListView.js';
|
|
7
7
|
// The api module is fully mocked; the view's App.tsx helpers stay real.
|
|
8
8
|
vi.mock('../api.ts', () => ({
|
|
9
9
|
listSessions: vi.fn(),
|
|
10
10
|
listWorkspaces: vi.fn(),
|
|
11
11
|
createSession: vi.fn(),
|
|
12
12
|
}));
|
|
13
|
-
import { createSession, listSessions, listWorkspaces } from
|
|
13
|
+
import { createSession, listSessions, listWorkspaces } from '../api.js';
|
|
14
14
|
const listSessionsMock = vi.mocked(listSessions);
|
|
15
15
|
const listWorkspacesMock = vi.mocked(listWorkspaces);
|
|
16
16
|
const createSessionMock = vi.mocked(createSession);
|
|
@@ -6,9 +6,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* until a workspace is opened).
|
|
7
7
|
*/
|
|
8
8
|
import { useEffect, useState } from 'react';
|
|
9
|
-
import { listWorkspaces } from
|
|
10
|
-
import { errorText } from
|
|
11
|
-
import { ThemeToggle } from
|
|
9
|
+
import { listWorkspaces } from '../api.js';
|
|
10
|
+
import { errorText } from './App.js';
|
|
11
|
+
import { ThemeToggle } from '../theme-toggle.js';
|
|
12
12
|
/**
|
|
13
13
|
* Render the workspace roster.
|
|
14
14
|
* @param props - the pick action.
|
|
@@ -3,12 +3,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
/** Mobile workspace landing: roster rendering and QR deep-link selection. */
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
-
import { mobileWorkspaceTarget } from
|
|
7
|
-
import { WorkspaceView } from
|
|
6
|
+
import { mobileWorkspaceTarget } from './App.js';
|
|
7
|
+
import { WorkspaceView } from './WorkspaceView.js';
|
|
8
8
|
vi.mock('../api.ts', () => ({
|
|
9
9
|
listWorkspaces: vi.fn(),
|
|
10
10
|
}));
|
|
11
|
-
import { listWorkspaces } from
|
|
11
|
+
import { listWorkspaces } from '../api.js';
|
|
12
12
|
const listWorkspacesMock = vi.mocked(listWorkspaces);
|
|
13
13
|
const workspaces = [
|
|
14
14
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAiDlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;aAC3B,MAAM,EAAE,mBAAmB;CACnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA4InE"}
|
package/lib/types/mobile-api.js
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* unary channel), gated identically.
|
|
20
20
|
*/
|
|
21
21
|
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc';
|
|
22
|
-
import {
|
|
22
|
+
import { readBoundedJson, writeJson } from './http.js';
|
|
23
|
+
import { readCookie } from './gate.js';
|
|
23
24
|
/** Methods the phone surface may call. Everything else is refused. */
|
|
24
25
|
const MOBILE_ALLOWLIST = new Set([
|
|
25
26
|
'workspace.list',
|
|
@@ -95,10 +96,6 @@ export function makeMobileApiRoutes(deps) {
|
|
|
95
96
|
const gateOk = (req) => {
|
|
96
97
|
return touchDeviceFor(req);
|
|
97
98
|
};
|
|
98
|
-
const writeJson = (res, status, body) => {
|
|
99
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
100
|
-
res.end(JSON.stringify(body));
|
|
101
|
-
};
|
|
102
99
|
const handleMethod = async (req, res) => {
|
|
103
100
|
if (req.method !== 'POST') {
|
|
104
101
|
res.writeHead(405);
|
|
@@ -143,7 +140,12 @@ export function makeMobileApiRoutes(deps) {
|
|
|
143
140
|
return;
|
|
144
141
|
}
|
|
145
142
|
try {
|
|
146
|
-
|
|
143
|
+
// Cancel the host-side work when the phone goes away mid-call (the
|
|
144
|
+
// response stream closing before we answer means nobody is listening).
|
|
145
|
+
const abort = new AbortController();
|
|
146
|
+
res.on('close', () => { if (!res.writableEnded)
|
|
147
|
+
abort.abort(); });
|
|
148
|
+
const response = await dispatch(apiProxy, method, parsed?.payload, rpcId, abort.signal);
|
|
147
149
|
writeJson(res, 200, response);
|
|
148
150
|
}
|
|
149
151
|
catch (error) {
|
|
@@ -222,19 +224,10 @@ export function makeMobileApiRoutes(deps) {
|
|
|
222
224
|
}
|
|
223
225
|
/** Read a request body as JSON (bounded). */
|
|
224
226
|
async function readJsonBody(req) {
|
|
225
|
-
|
|
226
|
-
let size = 0;
|
|
227
|
-
for await (const chunk of req) {
|
|
228
|
-
const buffer = chunk;
|
|
229
|
-
size += buffer.length;
|
|
230
|
-
if (size > 64 * 1024)
|
|
231
|
-
throw new Error('body too large');
|
|
232
|
-
chunks.push(buffer);
|
|
233
|
-
}
|
|
234
|
-
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
227
|
+
return readBoundedJson(req, 64 * 1024);
|
|
235
228
|
}
|
|
236
229
|
/** Dispatch one allowlisted method through the host ApiProxy. */
|
|
237
|
-
async function dispatch(apiProxy, method, payload, rpcId) {
|
|
230
|
+
async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
238
231
|
const request = { rpcId: RpcId(rpcId), payload };
|
|
239
232
|
if (method === 'session.list') {
|
|
240
233
|
const full = await apiProxy.sessions.list(request);
|
|
@@ -285,7 +278,7 @@ async function dispatch(apiProxy, method, payload, rpcId) {
|
|
|
285
278
|
if (method === 'session.history')
|
|
286
279
|
return wrap(await apiProxy.sessions.history(request));
|
|
287
280
|
if (method === 'session.search')
|
|
288
|
-
return wrap(await apiProxy.sessions.search(request, new AbortController().signal));
|
|
281
|
+
return wrap(await apiProxy.sessions.search(request, signal ?? new AbortController().signal));
|
|
289
282
|
if (method === 'session.prompt')
|
|
290
283
|
return wrap(await apiProxy.sessions.prompt(request));
|
|
291
284
|
if (method === 'session.models')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAwD/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAwD/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CAyC7C"}
|
|
@@ -67,19 +67,25 @@ export function makeMobileRoutes() {
|
|
|
67
67
|
const handlePage = (_req, res) => {
|
|
68
68
|
writeStatic(res, 200, 'text/html', pageHtml('/m/mobile.js'));
|
|
69
69
|
};
|
|
70
|
+
// The bundle is immutable for the process lifetime (a rebuild requires a
|
|
71
|
+
// host restart), so the ~456KB body is read from disk once, not per phone.
|
|
72
|
+
let bundleBody;
|
|
70
73
|
const handleBundle = async (_req, res) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
if (bundleBody === undefined) {
|
|
75
|
+
const path = mobileBundlePath();
|
|
76
|
+
if (!existsSync(path)) {
|
|
77
|
+
writeStatic(res, 503, 'text/plain', 'mobile bundle not built: run pnpm --filter @linxin666/dsh-remote-web-ui build');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
bundleBody = await readFile(path, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
writeStatic(res, 500, 'text/plain', 'failed to read the mobile bundle');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
82
87
|
}
|
|
88
|
+
writeStatic(res, 200, 'text/javascript', bundleBody);
|
|
83
89
|
};
|
|
84
90
|
const handleIcon = async (_req, res) => {
|
|
85
91
|
const path = appleTouchIconPath();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host single-instance guard shared by the plugin family. The family bundle
|
|
3
|
+
* (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
4
|
+
* the loader accepts a standalone install of the same package side by side;
|
|
5
|
+
* without this guard the second instance would still re-register the same
|
|
6
|
+
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
7
|
+
* and fail the boot. mountOnce makes the second host apply a no-op for the
|
|
8
|
+
* lifetime of the first instance (the browser half is already deduped by
|
|
9
|
+
* package name in the client module host).
|
|
10
|
+
*
|
|
11
|
+
* The registry rides a global symbol so two module instances of the same
|
|
12
|
+
* package (npm copy vs repository link) still share one verdict. cordis
|
|
13
|
+
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
14
|
+
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
18
|
+
* The first mount registers normally and unmarks when its fiber disposes;
|
|
19
|
+
* any later mount of the same package name is a no-op.
|
|
20
|
+
* @param packageName - npm package identity shared by every install source.
|
|
21
|
+
* @param fn - the original plugin apply.
|
|
22
|
+
* @returns an apply of the same shape.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mountOnce<T extends (...args: any[]) => unknown>(packageName: string, fn: T): T;
|
|
25
|
+
//# sourceMappingURL=mount-once.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount-once.d.ts","sourceRoot":"","sources":["../../src/mount-once.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;GAcG;AAaH;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAW9F"}
|