@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.10
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 +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +993 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- 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 +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +79 -39
- package/lib/types/client/locales.d.ts +32 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +42 -22
- package/lib/types/client/mobile-adapt.d.ts +49 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +1002 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +82 -39
- package/src/client/locales.ts +42 -22
- package/src/client/mobile-adapt.ts +954 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17658
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -149
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -561
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -63
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -821
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -685
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -268
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1342
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- package/src/paired-model-catalog.ts +0 -272
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
/** mux: live-event client, SSE delivery + stall-driven polling fallback. */
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { MuxClient } from "./mux.js";
|
|
4
|
-
/** Create an EventSource factory that records every opened source. */
|
|
5
|
-
function makeSources() {
|
|
6
|
-
const sources = [];
|
|
7
|
-
const factory = (url) => {
|
|
8
|
-
const source = {
|
|
9
|
-
url,
|
|
10
|
-
onmessage: null,
|
|
11
|
-
onerror: null,
|
|
12
|
-
closed: false,
|
|
13
|
-
close: () => { source.closed = true; },
|
|
14
|
-
};
|
|
15
|
-
sources.push(source);
|
|
16
|
-
return source;
|
|
17
|
-
};
|
|
18
|
-
return { factory, sources };
|
|
19
|
-
}
|
|
20
|
-
/** One history page whose events carry sequential ids. */
|
|
21
|
-
function pageOf(seqs) {
|
|
22
|
-
return {
|
|
23
|
-
hasMore: false,
|
|
24
|
-
events: seqs.map(seq => ({
|
|
25
|
-
event: { type: 'user/message', seq, time: seq * 1_000, data: { text: String(seq) } },
|
|
26
|
-
})),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/** A server-request envelope carrying one mux frame (the SSE wire shape). */
|
|
30
|
-
function envelopeWith(payload) {
|
|
31
|
-
return JSON.stringify({ type: 'server-request', rpcId: 'r1', method: 'events.mux', payload });
|
|
32
|
-
}
|
|
33
|
-
/** Options common to every test: tight clocks, injected data source. */
|
|
34
|
-
function baseOptions(pollLatest, factory) {
|
|
35
|
-
return { sourceFactory: factory, pollLatest, stallThresholdMs: 800, pollIntervalMs: 400 };
|
|
36
|
-
}
|
|
37
|
-
describe('MuxClient polling fallback', () => {
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
vi.useFakeTimers();
|
|
40
|
-
});
|
|
41
|
-
it('does not poll while the SSE channel is fresh', async () => {
|
|
42
|
-
const { factory } = makeSources();
|
|
43
|
-
const pollLatest = vi.fn(async (_sessionId) => pageOf([0]));
|
|
44
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
45
|
-
const frames = [];
|
|
46
|
-
client.onFrame(frame => { frames.push(frame); });
|
|
47
|
-
client.start();
|
|
48
|
-
client.observe('s1');
|
|
49
|
-
await vi.advanceTimersByTimeAsync(400); // well under the stall threshold
|
|
50
|
-
expect(pollLatest).not.toHaveBeenCalled();
|
|
51
|
-
expect(frames).toHaveLength(0);
|
|
52
|
-
client.stop();
|
|
53
|
-
});
|
|
54
|
-
it('starts polling after silence and emits appended events as session/event frames', async () => {
|
|
55
|
-
const { factory } = makeSources();
|
|
56
|
-
const pages = [pageOf([0]), pageOf([0, 1])];
|
|
57
|
-
const pollLatest = vi.fn(async (_sessionId) => pages.shift() ?? pageOf([]));
|
|
58
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
59
|
-
const frames = [];
|
|
60
|
-
client.onFrame(frame => { frames.push(frame); });
|
|
61
|
-
client.start();
|
|
62
|
-
client.observe('s1');
|
|
63
|
-
// Nothing live within a poll interval until the stall window passes.
|
|
64
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
65
|
-
expect(pollLatest).not.toHaveBeenCalled();
|
|
66
|
-
// The single tick (400 ms) crosses the 800 ms stall threshold at 1200 ms:
|
|
67
|
-
// the same tick then runs the first poll and emits seq 0.
|
|
68
|
-
await vi.advanceTimersByTimeAsync(800); // 1200ms total -> first stall crossing tick
|
|
69
|
-
expect(pollLatest).toHaveBeenCalledWith('s1');
|
|
70
|
-
expect(frames).toHaveLength(1);
|
|
71
|
-
expect(frames[0]).toMatchObject({ type: 'session/event', sessionId: 's1' });
|
|
72
|
-
expect(frames[0]?.event).toMatchObject({ seq: 0 });
|
|
73
|
-
// The next poll emits only the appended event (seq 1), not seq 0 again.
|
|
74
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
75
|
-
expect(frames).toHaveLength(2);
|
|
76
|
-
expect(frames[1]?.event).toMatchObject({ seq: 1 });
|
|
77
|
-
client.stop();
|
|
78
|
-
});
|
|
79
|
-
it('sorts an out-of-order history page before advancing the watermark', async () => {
|
|
80
|
-
const { factory } = makeSources();
|
|
81
|
-
const pollLatest = vi.fn(async (_sessionId) => pageOf([2, 1, 3]));
|
|
82
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
83
|
-
const seqs = [];
|
|
84
|
-
client.onFrame((frame) => {
|
|
85
|
-
if (frame.type === 'session/event' && typeof frame.event.seq === 'number')
|
|
86
|
-
seqs.push(frame.event.seq);
|
|
87
|
-
});
|
|
88
|
-
client.start();
|
|
89
|
-
client.observe('s1');
|
|
90
|
-
await vi.advanceTimersByTimeAsync(1200);
|
|
91
|
-
expect(seqs).toEqual([1, 2, 3]);
|
|
92
|
-
client.stop();
|
|
93
|
-
});
|
|
94
|
-
it('keeps the watermark so a repeated page never re-emits old events', async () => {
|
|
95
|
-
const { factory } = makeSources();
|
|
96
|
-
// Two calls return the same page: the second must emit nothing.
|
|
97
|
-
const pollLatest = vi.fn(async (_sessionId) => pageOf([0, 1, 2]));
|
|
98
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
99
|
-
const frames = [];
|
|
100
|
-
client.onFrame(frame => { frames.push(frame); });
|
|
101
|
-
client.start();
|
|
102
|
-
client.observe('s1');
|
|
103
|
-
await vi.advanceTimersByTimeAsync(1200); // first poll -> 3 events
|
|
104
|
-
expect(frames).toHaveLength(3);
|
|
105
|
-
await vi.advanceTimersByTimeAsync(400); // second poll -> same page, nothing new
|
|
106
|
-
expect(frames).toHaveLength(3);
|
|
107
|
-
client.stop();
|
|
108
|
-
});
|
|
109
|
-
it('stops polling when observe is cleared and keeps it stopped', async () => {
|
|
110
|
-
const { factory } = makeSources();
|
|
111
|
-
const pollLatest = vi.fn(async (_sessionId) => pageOf([0]));
|
|
112
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
113
|
-
client.start();
|
|
114
|
-
client.observe('s1');
|
|
115
|
-
await vi.advanceTimersByTimeAsync(1200);
|
|
116
|
-
expect(pollLatest).toHaveBeenCalled();
|
|
117
|
-
client.observe(undefined);
|
|
118
|
-
const callsAfterClear = pollLatest.mock.calls.length;
|
|
119
|
-
await vi.advanceTimersByTimeAsync(2000);
|
|
120
|
-
expect(pollLatest.mock.calls.length).toBe(callsAfterClear);
|
|
121
|
-
client.stop();
|
|
122
|
-
});
|
|
123
|
-
it('stops polling on stop(), closing any live source', async () => {
|
|
124
|
-
const { factory, sources } = makeSources();
|
|
125
|
-
const pollLatest = vi.fn(async (_sessionId) => pageOf([0]));
|
|
126
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
127
|
-
client.start();
|
|
128
|
-
client.observe('s1');
|
|
129
|
-
await vi.advanceTimersByTimeAsync(1200);
|
|
130
|
-
expect(pollLatest).toHaveBeenCalled();
|
|
131
|
-
client.stop();
|
|
132
|
-
const callsAfterStop = pollLatest.mock.calls.length;
|
|
133
|
-
await vi.advanceTimersByTimeAsync(2000);
|
|
134
|
-
expect(pollLatest.mock.calls.length).toBe(callsAfterStop);
|
|
135
|
-
expect(sources[0]?.closed).toBe(true);
|
|
136
|
-
});
|
|
137
|
-
it('returns to SSE when a frame arrives, dropping the fallback poller', async () => {
|
|
138
|
-
const { factory, sources } = makeSources();
|
|
139
|
-
const pollLatest = vi.fn(async (_sessionId) => pageOf([0]));
|
|
140
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
141
|
-
const frames = [];
|
|
142
|
-
client.onFrame(frame => { frames.push(frame); });
|
|
143
|
-
client.start();
|
|
144
|
-
client.observe('s1');
|
|
145
|
-
// Stall into polling.
|
|
146
|
-
await vi.advanceTimersByTimeAsync(1200);
|
|
147
|
-
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
148
|
-
// A live mux frame proves SSE delivers again -> fallback stops.
|
|
149
|
-
sources[0]?.onmessage?.({ data: envelopeWith({ type: 'session/subscribed', sessionId: 's1', lastSeq: 4 }) });
|
|
150
|
-
await vi.advanceTimersByTimeAsync(2000);
|
|
151
|
-
expect(pollLatest.mock.calls.length).toBe(1); // polling stopped after the live frame
|
|
152
|
-
const live = frames.filter(frame => frame?.type === 'session/subscribed');
|
|
153
|
-
expect(live).toHaveLength(1);
|
|
154
|
-
expect(live[0]).toMatchObject({ type: 'session/subscribed', sessionId: 's1' });
|
|
155
|
-
client.stop();
|
|
156
|
-
});
|
|
157
|
-
it('recovers when a previously-live SSE stream becomes silently stalled', async () => {
|
|
158
|
-
const { factory, sources } = makeSources();
|
|
159
|
-
const pollLatest = vi.fn(async (_sessionId) => pageOf([5]));
|
|
160
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
161
|
-
const frames = [];
|
|
162
|
-
client.onFrame(frame => { frames.push(frame); });
|
|
163
|
-
client.start();
|
|
164
|
-
client.observe('s1');
|
|
165
|
-
sources[0]?.onmessage?.({ data: envelopeWith({ type: 'session/subscribed', sessionId: 's1', lastSeq: 4 }) });
|
|
166
|
-
await vi.advanceTimersByTimeAsync(2400);
|
|
167
|
-
expect(pollLatest).not.toHaveBeenCalled();
|
|
168
|
-
// A once-live stream gets three stall windows; the next scheduler tick
|
|
169
|
-
// crosses that boundary and starts the ordinary-HTTP recovery path.
|
|
170
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
171
|
-
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
172
|
-
expect(frames.at(-1)?.event).toMatchObject({ seq: 5 });
|
|
173
|
-
client.stop();
|
|
174
|
-
});
|
|
175
|
-
it('backs empty polls off and resets to the base cadence after progress', async () => {
|
|
176
|
-
const { factory } = makeSources();
|
|
177
|
-
const pages = [pageOf([]), pageOf([1]), pageOf([1, 2])];
|
|
178
|
-
const pollLatest = vi.fn(async (_sessionId) => pages.shift() ?? pageOf([]));
|
|
179
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
180
|
-
client.start();
|
|
181
|
-
client.observe('s1');
|
|
182
|
-
await vi.advanceTimersByTimeAsync(1200);
|
|
183
|
-
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
184
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
185
|
-
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
186
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
187
|
-
expect(pollLatest).toHaveBeenCalledTimes(2);
|
|
188
|
-
// The productive second poll resets the next delay from 800 ms to 400 ms.
|
|
189
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
190
|
-
expect(pollLatest).toHaveBeenCalledTimes(3);
|
|
191
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
192
|
-
expect(pollLatest).toHaveBeenCalledTimes(4);
|
|
193
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
194
|
-
expect(pollLatest).toHaveBeenCalledTimes(4);
|
|
195
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
196
|
-
expect(pollLatest).toHaveBeenCalledTimes(5);
|
|
197
|
-
client.stop();
|
|
198
|
-
});
|
|
199
|
-
it('drives the whole lifecycle on a single scheduler tick (one interval)', async () => {
|
|
200
|
-
const { factory } = makeSources();
|
|
201
|
-
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
|
|
202
|
-
try {
|
|
203
|
-
const pages = [pageOf([0]), pageOf([0, 1])];
|
|
204
|
-
const pollLatest = vi.fn(async (_sessionId) => pages.shift() ?? pageOf([]));
|
|
205
|
-
const client = new MuxClient('/m/api/events.mux', {
|
|
206
|
-
sourceFactory: factory,
|
|
207
|
-
pollLatest,
|
|
208
|
-
stallThresholdMs: 1500,
|
|
209
|
-
pollIntervalMs: 2000,
|
|
210
|
-
});
|
|
211
|
-
client.start();
|
|
212
|
-
client.observe('s1');
|
|
213
|
-
// Exactly one interval is ever created: the single tick scheduler.
|
|
214
|
-
expect(setIntervalSpy).toHaveBeenCalledTimes(1);
|
|
215
|
-
// The tick fires below the stall threshold without polling.
|
|
216
|
-
await vi.advanceTimersByTimeAsync(1000);
|
|
217
|
-
expect(pollLatest).not.toHaveBeenCalled();
|
|
218
|
-
// The same interval arms the fallback once silence passes the threshold
|
|
219
|
-
// (tick at 2000 crosses 1500) and runs the first poll immediately.
|
|
220
|
-
await vi.advanceTimersByTimeAsync(1100); // 2100ms total
|
|
221
|
-
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
222
|
-
expect(setIntervalSpy).toHaveBeenCalledTimes(1); // still one timer
|
|
223
|
-
// Subsequent polls ride the same tick at the poll cadence (2000 ms).
|
|
224
|
-
await vi.advanceTimersByTimeAsync(2000);
|
|
225
|
-
expect(pollLatest).toHaveBeenCalledTimes(2);
|
|
226
|
-
client.stop();
|
|
227
|
-
}
|
|
228
|
-
finally {
|
|
229
|
-
setIntervalSpy.mockRestore();
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
it('paces polls on the tick only after the stall phase ends', async () => {
|
|
233
|
-
const { factory } = makeSources();
|
|
234
|
-
const pages = [pageOf([0]), pageOf([0, 1]), pageOf([0, 1, 2]), pageOf([0, 1, 2, 3])];
|
|
235
|
-
const pollLatest = vi.fn(async (_sessionId) => pages.shift() ?? pageOf([]));
|
|
236
|
-
const client = new MuxClient('/m/api/events.mux', {
|
|
237
|
-
sourceFactory: factory,
|
|
238
|
-
pollLatest,
|
|
239
|
-
stallThresholdMs: 800,
|
|
240
|
-
pollIntervalMs: 400,
|
|
241
|
-
});
|
|
242
|
-
client.start();
|
|
243
|
-
client.observe('s1');
|
|
244
|
-
// At 1200 ms the stall threshold is crossed on this single 400 ms tick.
|
|
245
|
-
await vi.advanceTimersByTimeAsync(1200);
|
|
246
|
-
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
247
|
-
// Each subsequent 400 ms tick is a poll, matching the poll cadence.
|
|
248
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
249
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
250
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
251
|
-
expect(pollLatest).toHaveBeenCalledTimes(4);
|
|
252
|
-
client.stop();
|
|
253
|
-
});
|
|
254
|
-
it('observe() already in the stall window starts the single-tick poller immediately', async () => {
|
|
255
|
-
const { factory } = makeSources();
|
|
256
|
-
const pollLatest = vi.fn(async (_sessionId) => pageOf([0]));
|
|
257
|
-
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
258
|
-
client.start();
|
|
259
|
-
// Advance well past the stall threshold while nothing is observed.
|
|
260
|
-
await vi.advanceTimersByTimeAsync(2000);
|
|
261
|
-
expect(pollLatest).not.toHaveBeenCalled();
|
|
262
|
-
// Observing a session now is already in the stall window: poll right away.
|
|
263
|
-
client.observe('s1');
|
|
264
|
-
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
265
|
-
client.stop();
|
|
266
|
-
});
|
|
267
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/** Pairing helpers owned by the standalone /m mobile surface. */
|
|
2
|
-
export type PairFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
3
|
-
export interface MobilePairInput {
|
|
4
|
-
token: string;
|
|
5
|
-
workspaceId?: string;
|
|
6
|
-
}
|
|
7
|
-
export type MobilePairAccept = {
|
|
8
|
-
ok: true;
|
|
9
|
-
} | {
|
|
10
|
-
ok: false;
|
|
11
|
-
message: string;
|
|
12
|
-
};
|
|
13
|
-
export type MobilePairBootstrap = {
|
|
14
|
-
kind: 'none';
|
|
15
|
-
} | {
|
|
16
|
-
kind: 'accepted';
|
|
17
|
-
path: string;
|
|
18
|
-
} | {
|
|
19
|
-
kind: 'failed';
|
|
20
|
-
path: string;
|
|
21
|
-
message: string;
|
|
22
|
-
};
|
|
23
|
-
/** Parse a pairing token or a copied pairing link without following its origin. */
|
|
24
|
-
export declare function parseMobilePairInput(value: string): MobilePairInput | undefined;
|
|
25
|
-
/** Build the safe, token-free mobile destination after successful pairing. */
|
|
26
|
-
export declare function mobilePairPath(workspaceId?: string): string;
|
|
27
|
-
/** Accept one pairing token on this exact browser or installed-web-app context. */
|
|
28
|
-
export declare function acceptMobilePair(token: string, fetcher?: PairFetch): Promise<MobilePairAccept>;
|
|
29
|
-
/** Consume a QR pairing token before the mobile application starts making RPC calls. */
|
|
30
|
-
export declare function consumeMobilePairUrl(href: string, fetcher?: PairFetch): Promise<MobilePairBootstrap>;
|
|
31
|
-
//# sourceMappingURL=pairing.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../../src/mobile/pairing.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAE3F,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAElC,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAIrD,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAgB/E;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,mFAAmF;AACnF,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,SAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAc3G;AAED,wFAAwF;AACxF,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,SAAiB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAejH"}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
const MOBILE_ROOT = '/m/';
|
|
2
|
-
/** Parse a pairing token or a copied pairing link without following its origin. */
|
|
3
|
-
export function parseMobilePairInput(value) {
|
|
4
|
-
const trimmed = value.trim();
|
|
5
|
-
if (trimmed === '')
|
|
6
|
-
return undefined;
|
|
7
|
-
try {
|
|
8
|
-
const url = new URL(trimmed);
|
|
9
|
-
const token = url.searchParams.get('pair');
|
|
10
|
-
if (token === null || token === '')
|
|
11
|
-
return undefined;
|
|
12
|
-
const workspaceId = url.searchParams.get('workspace');
|
|
13
|
-
return {
|
|
14
|
-
token,
|
|
15
|
-
...(workspaceId !== null && workspaceId !== '' ? { workspaceId } : {}),
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return { token: trimmed };
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
/** Build the safe, token-free mobile destination after successful pairing. */
|
|
23
|
-
export function mobilePairPath(workspaceId) {
|
|
24
|
-
return workspaceId === undefined ? MOBILE_ROOT : MOBILE_ROOT + '?workspace=' + encodeURIComponent(workspaceId);
|
|
25
|
-
}
|
|
26
|
-
/** Accept one pairing token on this exact browser or installed-web-app context. */
|
|
27
|
-
export async function acceptMobilePair(token, fetcher = fetch) {
|
|
28
|
-
try {
|
|
29
|
-
const response = await fetcher('/api/pair/accept', {
|
|
30
|
-
method: 'POST',
|
|
31
|
-
headers: { 'content-type': 'application/json' },
|
|
32
|
-
body: JSON.stringify({ token }),
|
|
33
|
-
});
|
|
34
|
-
if (response.ok)
|
|
35
|
-
return { ok: true };
|
|
36
|
-
if (response.status === 404)
|
|
37
|
-
return { ok: false, message: '配对链接无效或已过期。' };
|
|
38
|
-
if (response.status === 409)
|
|
39
|
-
return { ok: false, message: '配对链接已被使用。' };
|
|
40
|
-
return { ok: false, message: '此设备无法使用该配对链接。' };
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return { ok: false, message: '无法连接到配对服务。' };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/** Consume a QR pairing token before the mobile application starts making RPC calls. */
|
|
47
|
-
export async function consumeMobilePairUrl(href, fetcher = fetch) {
|
|
48
|
-
let url;
|
|
49
|
-
try {
|
|
50
|
-
url = new URL(href);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return { kind: 'none' };
|
|
54
|
-
}
|
|
55
|
-
const token = url.searchParams.get('pair');
|
|
56
|
-
if (token === null || token === '')
|
|
57
|
-
return { kind: 'none' };
|
|
58
|
-
const workspaceId = url.searchParams.get('workspace');
|
|
59
|
-
const path = mobilePairPath(workspaceId === null || workspaceId === '' ? undefined : workspaceId);
|
|
60
|
-
const result = await acceptMobilePair(token, fetcher);
|
|
61
|
-
return result.ok ? { kind: 'accepted', path } : { kind: 'failed', path, message: result.message };
|
|
62
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile-surface unary RPC over the shared /api transport: the four-quadrant
|
|
3
|
-
* envelope (client-request → server-response), minted rpcIds, and typed
|
|
4
|
-
* error mapping. This is a thin, self-contained slice of the harness
|
|
5
|
-
* apiproxy fetch carrier — the mobile page is an independent bundle and must
|
|
6
|
-
* not depend on the main UI's module loader, so the wire contract is
|
|
7
|
-
* reimplemented here over plain fetch.
|
|
8
|
-
*/
|
|
9
|
-
/** Transport-level failure (network, HTTP status, malformed envelope). */
|
|
10
|
-
export declare class RpcTransportError extends Error {
|
|
11
|
-
constructor(message: string);
|
|
12
|
-
}
|
|
13
|
-
/** A business error the host answered with (200 + err result). */
|
|
14
|
-
export declare class RpcCallError extends Error {
|
|
15
|
-
/** The wire error (code + message + details). */
|
|
16
|
-
readonly error: {
|
|
17
|
-
code: string;
|
|
18
|
-
message: string;
|
|
19
|
-
};
|
|
20
|
-
constructor(error: {
|
|
21
|
-
code: string;
|
|
22
|
-
message: string;
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
/** Mint one process-unique rpcId (stable under crypto.randomUUID absence). */
|
|
26
|
-
export declare function mintRpcId(): string;
|
|
27
|
-
/**
|
|
28
|
-
* One unary call: POST /m/api/<method> (the plugin's own mobile channel —
|
|
29
|
-
* NOT the connection plugin's /api prefix, so the tunneled Host never needs
|
|
30
|
-
* to enter the transport trust fence) with the client-request envelope,
|
|
31
|
-
* resolve the server-response value, reject with the mapped error classes.
|
|
32
|
-
* @param method - the dotted RPC method, e.g. `session.list`.
|
|
33
|
-
* @param payload - the business payload.
|
|
34
|
-
* @param signal - optional abort.
|
|
35
|
-
* @returns the response value.
|
|
36
|
-
*/
|
|
37
|
-
export declare function callUnary<T>(method: string, payload: unknown, signal?: AbortSignal): Promise<T>;
|
|
38
|
-
//# sourceMappingURL=rpc.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../../src/mobile/rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,0EAA0E;AAC1E,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;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;gBAErC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;CAKrD;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"}
|
package/lib/types/mobile/rpc.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile-surface unary RPC over the shared /api transport: the four-quadrant
|
|
3
|
-
* envelope (client-request → server-response), minted rpcIds, and typed
|
|
4
|
-
* error mapping. This is a thin, self-contained slice of the harness
|
|
5
|
-
* apiproxy fetch carrier — the mobile page is an independent bundle and must
|
|
6
|
-
* not depend on the main UI's module loader, so the wire contract is
|
|
7
|
-
* reimplemented here over plain fetch.
|
|
8
|
-
*/
|
|
9
|
-
/** Transport-level failure (network, HTTP status, malformed envelope). */
|
|
10
|
-
export class RpcTransportError extends Error {
|
|
11
|
-
constructor(message) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = 'RpcTransportError';
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/** A business error the host answered with (200 + err result). */
|
|
17
|
-
export class RpcCallError extends Error {
|
|
18
|
-
/** The wire error (code + message + details). */
|
|
19
|
-
error;
|
|
20
|
-
constructor(error) {
|
|
21
|
-
super(error.message);
|
|
22
|
-
this.name = 'RpcCallError';
|
|
23
|
-
this.error = error;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
let rpcCounter = 0;
|
|
27
|
-
/** Mint one process-unique rpcId (stable under crypto.randomUUID absence). */
|
|
28
|
-
export function mintRpcId() {
|
|
29
|
-
const random = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
30
|
-
? crypto.randomUUID()
|
|
31
|
-
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
32
|
-
rpcCounter += 1;
|
|
33
|
-
return `${random}-${rpcCounter.toString(36)}`;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* One unary call: POST /m/api/<method> (the plugin's own mobile channel —
|
|
37
|
-
* NOT the connection plugin's /api prefix, so the tunneled Host never needs
|
|
38
|
-
* to enter the transport trust fence) with the client-request envelope,
|
|
39
|
-
* resolve the server-response value, reject with the mapped error classes.
|
|
40
|
-
* @param method - the dotted RPC method, e.g. `session.list`.
|
|
41
|
-
* @param payload - the business payload.
|
|
42
|
-
* @param signal - optional abort.
|
|
43
|
-
* @returns the response value.
|
|
44
|
-
*/
|
|
45
|
-
export async function callUnary(method, payload, signal) {
|
|
46
|
-
const rpcId = mintRpcId();
|
|
47
|
-
let response;
|
|
48
|
-
try {
|
|
49
|
-
response = await fetch(`/m/api/${method}`, {
|
|
50
|
-
method: 'POST',
|
|
51
|
-
headers: { 'content-type': 'application/json' },
|
|
52
|
-
body: JSON.stringify({ type: 'client-request', rpcId, method, payload }),
|
|
53
|
-
...(signal !== undefined ? { signal } : {}),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
const reason = error instanceof DOMException && error.name === 'AbortError'
|
|
58
|
-
? 'aborted'
|
|
59
|
-
: error instanceof Error ? error.message : String(error);
|
|
60
|
-
throw new RpcTransportError(`transport failed: ${reason}`);
|
|
61
|
-
}
|
|
62
|
-
if (!response.ok) {
|
|
63
|
-
throw new RpcTransportError(`HTTP ${String(response.status)}`);
|
|
64
|
-
}
|
|
65
|
-
let envelope;
|
|
66
|
-
try {
|
|
67
|
-
envelope = await response.json();
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
throw new RpcTransportError('malformed response body');
|
|
71
|
-
}
|
|
72
|
-
const parsed = envelope;
|
|
73
|
-
if (parsed?.type !== 'server-response' || parsed.rpcId !== rpcId) {
|
|
74
|
-
throw new RpcTransportError('response envelope mismatch');
|
|
75
|
-
}
|
|
76
|
-
const result = parsed.result;
|
|
77
|
-
if (result?.ok === true)
|
|
78
|
-
return result.value;
|
|
79
|
-
if (result?.ok === false && result.error !== undefined) {
|
|
80
|
-
throw new RpcCallError(result.error);
|
|
81
|
-
}
|
|
82
|
-
throw new RpcTransportError('malformed result envelope');
|
|
83
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme toggle button: sits at the right end of every view header and flips
|
|
3
|
-
* the mobile surface between the light (default) and dark palettes. The
|
|
4
|
-
* glyph shows the theme you will switch TO (moon in light mode, sun in dark
|
|
5
|
-
* mode); the choice persists across visits.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Render the header theme toggle.
|
|
9
|
-
* @returns the toggle button.
|
|
10
|
-
*/
|
|
11
|
-
export declare function ThemeToggle(): import("react").JSX.Element;
|
|
12
|
-
//# sourceMappingURL=theme-toggle.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"theme-toggle.d.ts","sourceRoot":"","sources":["../../../src/mobile/theme-toggle.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwBH;;;GAGG;AACH,wBAAgB,WAAW,gCAa1B"}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Theme toggle button: sits at the right end of every view header and flips
|
|
4
|
-
* the mobile surface between the light (default) and dark palettes. The
|
|
5
|
-
* glyph shows the theme you will switch TO (moon in light mode, sun in dark
|
|
6
|
-
* mode); the choice persists across visits.
|
|
7
|
-
*/
|
|
8
|
-
import { useSyncExternalStore } from 'react';
|
|
9
|
-
import { getMobileTheme, subscribeMobileTheme, toggleMobileTheme } from "./mobile-theme.js";
|
|
10
|
-
/** Sun glyph: circle + eight rays (inline SVG, no emoji). */
|
|
11
|
-
function SunIcon() {
|
|
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" })] }));
|
|
13
|
-
}
|
|
14
|
-
/** Moon glyph: crescent path (inline SVG, no emoji). */
|
|
15
|
-
function MoonIcon() {
|
|
16
|
-
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: _jsx("path", { d: "M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5Z" }) }));
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Render the header theme toggle.
|
|
20
|
-
* @returns the toggle button.
|
|
21
|
-
*/
|
|
22
|
-
export function ThemeToggle() {
|
|
23
|
-
const theme = useSyncExternalStore(subscribeMobileTheme, getMobileTheme);
|
|
24
|
-
const dark = theme === 'dark';
|
|
25
|
-
return (_jsx("button", { type: "button", className: "mobile-theme-toggle", "aria-label": dark ? '切换到浅色' : '切换到深色', onClick: () => { toggleMobileTheme(); }, children: dark ? _jsx(SunIcon, {}) : _jsx(MoonIcon, {}) }));
|
|
26
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile surface root: the view state machine (workspaces → sessions →
|
|
3
|
-
* chat) and the top-level data flows. Deliberately plain React state — no
|
|
4
|
-
* router, no state library: the surface is three fixed levels with a back
|
|
5
|
-
* affordance, and every piece of data is fetched on demand.
|
|
6
|
-
*/
|
|
7
|
-
import { listSessions, listWorkspaces, prompt } from '../api.ts';
|
|
8
|
-
/** The session-list row model (list + chat share it). */
|
|
9
|
-
export interface SessionView {
|
|
10
|
-
sessionId: string;
|
|
11
|
-
title: string;
|
|
12
|
-
cwd?: string;
|
|
13
|
-
updatedAt: number;
|
|
14
|
-
running: boolean;
|
|
15
|
-
blank: boolean;
|
|
16
|
-
}
|
|
17
|
-
/** Read the optional workspace target carried from the pairing QR flow. */
|
|
18
|
-
export declare function mobileWorkspaceTarget(search: string): string | undefined;
|
|
19
|
-
/** Map a list row to the surface model; the title comes from projections when present. */
|
|
20
|
-
export declare function toSessionView(item: {
|
|
21
|
-
sessionId: string;
|
|
22
|
-
updatedAt: number;
|
|
23
|
-
running: boolean;
|
|
24
|
-
blank: boolean;
|
|
25
|
-
cwd?: string;
|
|
26
|
-
projections?: {
|
|
27
|
-
values?: Record<string, unknown>;
|
|
28
|
-
};
|
|
29
|
-
}): SessionView;
|
|
30
|
-
/** Human clock, e.g. "14:05" or "昨天 20:31". */
|
|
31
|
-
export declare function formatTime(epochMs: number): string;
|
|
32
|
-
/** Props accepted by the mobile root before it begins paired-device RPC calls. */
|
|
33
|
-
export interface AppProps {
|
|
34
|
-
initialPairError?: string;
|
|
35
|
-
}
|
|
36
|
-
/** Whether the mobile gateway rejected this browser for lack of a paired cookie. */
|
|
37
|
-
export declare function isUnpairedMobileError(error: unknown): boolean;
|
|
38
|
-
/** The result of probing the paired-device-only mobile preference endpoint. */
|
|
39
|
-
export type MobilePairState = 'checking' | 'paired' | 'unpaired' | 'unavailable';
|
|
40
|
-
/** Classify an initial mobile preference failure without treating outage as authorization. */
|
|
41
|
-
export declare function mobilePairStateForError(error: unknown): Extract<MobilePairState, 'unpaired' | 'unavailable'>;
|
|
42
|
-
/** Gate the independent mobile bundle until its own browser context is paired. */
|
|
43
|
-
export declare function App({ initialPairError }: AppProps): import("react").JSX.Element;
|
|
44
|
-
/** Shared error text for the surface's small failure affordances. */
|
|
45
|
-
export declare function errorText(error: unknown): string;
|
|
46
|
-
/**
|
|
47
|
-
* Actionable hint for transport-level 403s on host-gated channels (model
|
|
48
|
-
* picker, session creation): the phone's UI bundle is served fresh from disk
|
|
49
|
-
* per request, while the host-side allowlist lives in the long-running
|
|
50
|
-
* process — a rebuild without a restart shows the new surface against the
|
|
51
|
-
* old allowlist (HTTP 403 forbidden).
|
|
52
|
-
*/
|
|
53
|
-
export declare function staleHostHint(message: string): string | undefined;
|
|
54
|
-
/** Fetch one history page (tail by default) — thin wrapper so views share the call shape. */
|
|
55
|
-
export declare function loadHistory(sessionId: string, beforeSeq?: number, signal?: AbortSignal): Promise<import("../api.ts").HistoryPage>;
|
|
56
|
-
export { listSessions, listWorkspaces, prompt };
|
|
57
|
-
//# sourceMappingURL=App.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAmD,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAcjH,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,kFAAkF;AAClF,MAAM,WAAW,QAAQ;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED,+EAA+E;AAC/E,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,aAAa,CAAA;AAEhF,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,EAAE,UAAU,GAAG,aAAa,CAAC,CAE5G;AAED,kFAAkF;AAClF,wBAAgB,GAAG,CAAC,EAAE,gBAAgB,EAAE,EAAE,QAAQ,+BA4BjD;AAiED,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"}
|