@linxin666/dsh-remote-web-ui 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +4 -1
  2. package/lib/client.js +35 -0
  3. package/lib/client.js.map +1 -0
  4. package/lib/mobile.js +15467 -0
  5. package/lib/mobile.js.map +1 -0
  6. package/lib/types/client/FooterRemoteEntry.d.ts +22 -0
  7. package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -0
  8. package/lib/types/client/FooterRemoteEntry.js +10 -0
  9. package/lib/types/client/PairFailedNotice.js +24 -0
  10. package/lib/types/client/PairFailedNotice.js.map +1 -0
  11. package/lib/types/client/PhoneIcon.js +9 -0
  12. package/lib/types/client/PhoneIcon.js.map +1 -0
  13. package/lib/types/client/PluginSettingsCard.js +39 -0
  14. package/lib/types/client/PluginSettingsCard.js.map +1 -0
  15. package/lib/types/client/RemoteEntry.js +162 -0
  16. package/lib/types/client/RemoteEntry.js.map +1 -0
  17. package/lib/types/client/RemotePanel.js +34 -0
  18. package/lib/types/client/RemotePanel.js.map +1 -0
  19. package/lib/types/client/RemoteSettingsCard.js +59 -0
  20. package/lib/types/client/RemoteSettingsCard.js.map +1 -0
  21. package/lib/types/client/deep-link.js +118 -0
  22. package/lib/types/client/deep-link.js.map +1 -0
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +134 -0
  25. package/lib/types/client/index.js.map +1 -0
  26. package/lib/types/client/locales.js +141 -0
  27. package/lib/types/client/locales.js.map +1 -0
  28. package/lib/types/client/pair-api.js +123 -0
  29. package/lib/types/client/pair-api.js.map +1 -0
  30. package/lib/types/client/settings-form.js +211 -0
  31. package/lib/types/client/settings-form.js.map +1 -0
  32. package/lib/types/gate.js +121 -0
  33. package/lib/types/gate.js.map +1 -0
  34. package/lib/types/index.js +227 -0
  35. package/lib/types/index.js.map +1 -0
  36. package/lib/types/invariant.js +23 -0
  37. package/lib/types/invariant.js.map +1 -0
  38. package/lib/types/lan.js +19 -0
  39. package/lib/types/lan.js.map +1 -0
  40. package/lib/types/mobile/api.js +60 -0
  41. package/lib/types/mobile/api.js.map +1 -0
  42. package/lib/types/mobile/index.js +25 -0
  43. package/lib/types/mobile/index.js.map +1 -0
  44. package/lib/types/mobile/messages.js +449 -0
  45. package/lib/types/mobile/messages.js.map +1 -0
  46. package/lib/types/mobile/messages.test.js +189 -0
  47. package/lib/types/mobile/messages.test.js.map +1 -0
  48. package/lib/types/mobile/mobile-styles.js +853 -0
  49. package/lib/types/mobile/mobile-styles.js.map +1 -0
  50. package/lib/types/mobile/mobile-theme.js +70 -0
  51. package/lib/types/mobile/mobile-theme.test.js +71 -0
  52. package/lib/types/mobile/mux.js +110 -0
  53. package/lib/types/mobile/mux.js.map +1 -0
  54. package/lib/types/mobile/rpc.js +83 -0
  55. package/lib/types/mobile/rpc.js.map +1 -0
  56. package/lib/types/mobile/theme-toggle.js +26 -0
  57. package/lib/types/mobile/views/App.js +102 -0
  58. package/lib/types/mobile/views/App.js.map +1 -0
  59. package/lib/types/mobile/views/ChatView.js +348 -0
  60. package/lib/types/mobile/views/ChatView.js.map +1 -0
  61. package/lib/types/mobile/views/ChatView.test.js +215 -0
  62. package/lib/types/mobile/views/SessionListView.js +108 -0
  63. package/lib/types/mobile/views/SessionListView.js.map +1 -0
  64. package/lib/types/mobile/views/SessionListView.test.js +110 -0
  65. package/lib/types/mobile/views/WorkspaceView.js +43 -0
  66. package/lib/types/mobile/views/WorkspaceView.js.map +1 -0
  67. package/lib/types/mobile-api.js +263 -0
  68. package/lib/types/mobile-api.js.map +1 -0
  69. package/lib/types/mobile-routes.js +71 -0
  70. package/lib/types/mobile-routes.js.map +1 -0
  71. package/lib/types/pairing.js +285 -0
  72. package/lib/types/pairing.js.map +1 -0
  73. package/lib/types/routes.js +327 -0
  74. package/lib/types/routes.js.map +1 -0
  75. package/lib/types/tunnel.js +222 -0
  76. package/lib/types/tunnel.js.map +1 -0
  77. package/package.json +9 -6
  78. package/src/client/FooterRemoteEntry.tsx +31 -0
  79. package/src/client/index.ts +23 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile-styles.js","sourceRoot":"","sources":["../../../src/mobile/mobile-styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuUxB,CAAA"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Mobile-surface theme: light by default, dark via an explicit persisted
3
+ * toggle. The standalone page boots without the shell, so there is no theme
4
+ * system to inherit — the choice is stored in localStorage and applied as a
5
+ * `data-theme` attribute on <html>; the stylesheet defines both palettes
6
+ * under `:root` (light) / `:root[data-theme='dark']` (dark).
7
+ *
8
+ * A tiny module store (subscribe/get) keeps the toggle button and the boot
9
+ * path in sync without threading props through the three view levels.
10
+ */
11
+ const STORAGE_KEY = 'dsh.remote.theme';
12
+ const THEME_COLOR_META_NAME = 'theme-color';
13
+ const LIGHT_THEME_COLOR = '#f3f5f9';
14
+ const DARK_THEME_COLOR = '#111418';
15
+ let current = readStored() ?? 'light';
16
+ const listeners = new Set();
17
+ function readStored() {
18
+ try {
19
+ const value = localStorage.getItem(STORAGE_KEY);
20
+ return value === 'dark' || value === 'light' ? value : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ function persist(theme) {
27
+ try {
28
+ localStorage.setItem(STORAGE_KEY, theme);
29
+ }
30
+ catch {
31
+ // Private-mode storage failures are non-fatal; the session keeps the theme.
32
+ }
33
+ }
34
+ /** Apply the theme to the document: data-theme attribute + browser chrome color. */
35
+ function applyToDocument(theme) {
36
+ if (typeof document === 'undefined')
37
+ return;
38
+ document.documentElement.dataset.theme = theme;
39
+ const meta = document.querySelector(`meta[name="${THEME_COLOR_META_NAME}"]`);
40
+ if (meta !== null)
41
+ meta.content = theme === 'dark' ? DARK_THEME_COLOR : LIGHT_THEME_COLOR;
42
+ }
43
+ /** Current theme (light unless the user explicitly toggled to dark). */
44
+ export function getMobileTheme() {
45
+ return current;
46
+ }
47
+ /** Subscribe to theme changes; returns the unsubscribe function. */
48
+ export function subscribeMobileTheme(listener) {
49
+ listeners.add(listener);
50
+ return () => { listeners.delete(listener); };
51
+ }
52
+ /** Set the theme explicitly (persisted + applied to the document). */
53
+ export function setMobileTheme(theme) {
54
+ if (theme === current)
55
+ return;
56
+ current = theme;
57
+ persist(theme);
58
+ applyToDocument(theme);
59
+ for (const listener of [...listeners])
60
+ listener();
61
+ }
62
+ /** Flip light/dark and return the new theme. */
63
+ export function toggleMobileTheme() {
64
+ setMobileTheme(current === 'light' ? 'dark' : 'light');
65
+ return current;
66
+ }
67
+ /** Apply the persisted (or default) theme once at boot, before first paint. */
68
+ export function initMobileTheme() {
69
+ applyToDocument(current);
70
+ }
@@ -0,0 +1,71 @@
1
+ // @vitest-environment jsdom
2
+ /** mobile-theme: light default, persisted dark toggle, document wiring. */
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { getMobileTheme, setMobileTheme, subscribeMobileTheme, toggleMobileTheme, } from "./mobile-theme.js";
5
+ const STORAGE_KEY = 'dsh.remote.theme';
6
+ /** jsdom in this setup ships a bare localStorage object; install a real fake. */
7
+ function makeStorage() {
8
+ const map = new Map();
9
+ return {
10
+ getItem: (key) => map.get(key) ?? null,
11
+ setItem: (key, value) => { map.set(key, value); },
12
+ removeItem: (key) => { map.delete(key); },
13
+ clear: () => { map.clear(); },
14
+ key: (index) => [...map.keys()][index] ?? null,
15
+ get length() { return map.size; },
16
+ };
17
+ }
18
+ let storage;
19
+ beforeEach(() => {
20
+ storage = makeStorage();
21
+ vi.stubGlobal('localStorage', storage);
22
+ setMobileTheme('light');
23
+ document.documentElement.removeAttribute('data-theme');
24
+ });
25
+ afterEach(() => {
26
+ vi.unstubAllGlobals();
27
+ });
28
+ describe('mobile-theme', () => {
29
+ it('defaults to light with nothing stored', () => {
30
+ expect(getMobileTheme()).toBe('light');
31
+ });
32
+ it('toggles to dark, persists it, and wires the document', () => {
33
+ expect(toggleMobileTheme()).toBe('dark');
34
+ expect(getMobileTheme()).toBe('dark');
35
+ expect(document.documentElement.dataset.theme).toBe('dark');
36
+ expect(storage.getItem(STORAGE_KEY)).toBe('dark');
37
+ });
38
+ it('toggles back to light', () => {
39
+ toggleMobileTheme();
40
+ expect(toggleMobileTheme()).toBe('light');
41
+ expect(document.documentElement.dataset.theme).toBe('light');
42
+ expect(storage.getItem(STORAGE_KEY)).toBe('light');
43
+ });
44
+ it('updates the browser chrome color along with the theme', () => {
45
+ const meta = document.createElement('meta');
46
+ meta.name = 'theme-color';
47
+ document.head.appendChild(meta);
48
+ setMobileTheme('dark');
49
+ expect(meta.content).toBe('#111418');
50
+ setMobileTheme('light');
51
+ expect(meta.content).toBe('#f3f5f9');
52
+ meta.remove();
53
+ });
54
+ it('boot reads the stored theme and initMobileTheme applies it', async () => {
55
+ storage.setItem(STORAGE_KEY, 'dark');
56
+ vi.resetModules();
57
+ const fresh = await import("./mobile-theme.js");
58
+ expect(fresh.getMobileTheme()).toBe('dark');
59
+ fresh.initMobileTheme();
60
+ expect(document.documentElement.dataset.theme).toBe('dark');
61
+ });
62
+ it('notifies subscribers on change', () => {
63
+ const seen = [];
64
+ const unsubscribe = subscribeMobileTheme(() => { seen.push(getMobileTheme()); });
65
+ setMobileTheme('dark');
66
+ setMobileTheme('light');
67
+ unsubscribe();
68
+ setMobileTheme('dark');
69
+ expect(seen).toEqual(['dark', 'light']);
70
+ });
71
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Mobile-surface live-event client: the plugin's `/m/api/events.mux` SSE
3
+ * channel (Server-Sent Events — the host bridges the mux stream onto it, so
4
+ * no WebSocket handshake or framing is needed on this side). The host
5
+ * pushes mux frames (subscribed baselines, session events, approvals,
6
+ * questions, queue snapshots, tasks, projections) as soon as the stream
7
+ * opens — no subscription handshake is needed. Frames arrive as
8
+ * server-request envelopes whose payload is the mux frame; unknown frame
9
+ * types are dropped so a newer host never breaks this client.
10
+ * EventSource reconnects automatically.
11
+ */
12
+ import { muxFrameSchema } from '@deepseek-ai/dsh-host-apiproxy/api/events.schema';
13
+ import { serverRequestSchema } from '@deepseek-ai/dsh-host-apiproxy/api/rpc.schema';
14
+ /** Browser default source factory. */
15
+ function browserSource(url) {
16
+ // The DOM EventSource is structurally compatible; the `this`-typed handler
17
+ // signatures differ, so the narrow face takes it through an adapter cast.
18
+ return new EventSource(url);
19
+ }
20
+ /**
21
+ * Keep one SSE subscription open, fanning validated frames out to
22
+ * subscribers. EventSource owns reconnection (with its own backoff); this
23
+ * class only manages the subscription lifecycle.
24
+ */
25
+ export class MuxClient {
26
+ sourceFactory;
27
+ listeners = new Set();
28
+ source;
29
+ stopped = false;
30
+ url;
31
+ /**
32
+ * @param url - the mobile events endpoint (browser-relative).
33
+ * @param options - seams.
34
+ */
35
+ constructor(url = '/m/api/events.mux', options = {}) {
36
+ this.url = url;
37
+ this.sourceFactory = options.sourceFactory ?? browserSource;
38
+ }
39
+ /** Open the stream (idempotent; EventSource reconnects until {@link stop}). */
40
+ start() {
41
+ this.stopped = false;
42
+ if (this.source !== undefined)
43
+ return;
44
+ this.connect();
45
+ }
46
+ /** Close for good. */
47
+ stop() {
48
+ this.stopped = true;
49
+ this.closeSource();
50
+ }
51
+ /** Subscribe to validated frames; returns an unsubscribe function. */
52
+ onFrame(listener) {
53
+ this.listeners.add(listener);
54
+ return () => { this.listeners.delete(listener); };
55
+ }
56
+ connect() {
57
+ const source = this.sourceFactory(this.url);
58
+ this.source = source;
59
+ source.onmessage = (event) => {
60
+ this.handleMessage(event.data);
61
+ };
62
+ source.onerror = () => {
63
+ // EventSource reconnects by itself; when we are closing, detach first
64
+ // so the native reconnect cannot outlive stop().
65
+ if (this.stopped && this.source === source)
66
+ this.closeSource();
67
+ };
68
+ }
69
+ handleMessage(data) {
70
+ if (typeof data !== 'string' || data === '')
71
+ return;
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(data);
75
+ }
76
+ catch {
77
+ return;
78
+ }
79
+ // The SSE channel carries server-request envelopes whose payload is the
80
+ // mux frame (same wire shape as the desktop mux channel).
81
+ const envelope = serverRequestSchema.safeParse(parsed);
82
+ if (!envelope.success)
83
+ return;
84
+ const frame = muxFrameSchema.safeParse(envelope.data.payload);
85
+ if (!frame.success)
86
+ return;
87
+ for (const listener of this.listeners) {
88
+ try {
89
+ listener(frame.data);
90
+ }
91
+ catch {
92
+ // A throwing subscriber must not break the emit loop.
93
+ }
94
+ }
95
+ }
96
+ closeSource() {
97
+ const source = this.source;
98
+ this.source = undefined;
99
+ if (source !== undefined) {
100
+ source.onmessage = null;
101
+ source.onerror = null;
102
+ try {
103
+ source.close();
104
+ }
105
+ catch {
106
+ // Already closed.
107
+ }
108
+ }
109
+ }
110
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mux.js","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,kDAAkD,CAAA;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAA;AAenF,sCAAsC;AACtC,SAAS,aAAa,CAAC,GAAW;IAChC,2EAA2E;IAC3E,0EAA0E;IAC1E,OAAO,IAAI,WAAW,CAAC,GAAG,CAA+B,CAAA;AAC3D,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,SAAS;IACH,aAAa,CAAkC;IAC/C,SAAS,GAAG,IAAI,GAAG,EAA6B,CAAA;IACzD,MAAM,CAA6B;IACnC,OAAO,GAAG,KAAK,CAAA;IACN,GAAG,CAAQ;IAE5B;;;OAGG;IACH,YAAY,GAAG,GAAG,mBAAmB,EAAE,UAA4B,EAAE;QACnE,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,aAAa,CAAA;IAC7D,CAAC;IAED,+EAA+E;IAC/E,KAAK;QACH,IAAI,CAAC,OAAO,GAAG,KAAK,CAAA;QACpB,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;YAAE,OAAM;QACrC,IAAI,CAAC,OAAO,EAAE,CAAA;IAChB,CAAC;IAED,sBAAsB;IACtB,IAAI;QACF,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACnB,IAAI,CAAC,WAAW,EAAE,CAAA;IACpB,CAAC;IAED,sEAAsE;IACtE,OAAO,CAAC,QAAmC;QACzC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAC5B,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA,CAAC,CAAC,CAAA;IAClD,CAAC;IAEO,OAAO;QACb,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC3C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,MAAM,CAAC,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE;YAC3B,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAChC,CAAC,CAAA;QACD,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE;YACpB,sEAAsE;YACtE,iDAAiD;YACjD,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM;gBAAE,IAAI,CAAC,WAAW,EAAE,CAAA;QAChE,CAAC,CAAA;IACH,CAAC;IAEO,aAAa,CAAC,IAAY;QAChC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE;YAAE,OAAM;QACnD,IAAI,MAAe,CAAA;QACnB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,OAAM;QACR,CAAC;QACD,wEAAwE;QACxE,0DAA0D;QAC1D,MAAM,QAAQ,GAAG,mBAAmB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;QACtD,IAAI,CAAC,QAAQ,CAAC,OAAO;YAAE,OAAM;QAC7B,MAAM,KAAK,GAAG,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC7D,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAM;QAC1B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACtB,CAAC;YAAC,MAAM,CAAC;gBACP,sDAAsD;YACxD,CAAC;QACH,CAAC;IACH,CAAC;IAEO,WAAW;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;QAC1B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,CAAC,SAAS,GAAG,IAAI,CAAA;YACvB,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;YACrB,IAAI,CAAC;gBACH,MAAM,CAAC,KAAK,EAAE,CAAA;YAChB,CAAC;YAAC,MAAM,CAAC;gBACP,kBAAkB;YACpB,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,83 @@
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
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../../../src/mobile/rpc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,102 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Mobile surface root: the view state machine (workspaces → sessions →
4
+ * chat) and the top-level data flows. Deliberately plain React state — no
5
+ * router, no state library: the surface is three fixed levels with a back
6
+ * affordance, and every piece of data is fetched on demand.
7
+ */
8
+ import { useCallback, useEffect, useRef, useState } from 'react';
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
+ /** Map a list row to the surface model; the title comes from projections when present. */
16
+ export function toSessionView(item) {
17
+ const titleValue = item.projections?.values?.title;
18
+ const title = typeof titleValue === 'string' && titleValue !== ''
19
+ ? titleValue
20
+ : item.cwd !== undefined ? item.cwd.split('/').filter(Boolean).at(-1) ?? item.cwd : '新会话';
21
+ return {
22
+ sessionId: item.sessionId,
23
+ title,
24
+ ...(item.cwd !== undefined ? { cwd: item.cwd } : {}),
25
+ updatedAt: item.updatedAt,
26
+ running: item.running,
27
+ blank: item.blank,
28
+ };
29
+ }
30
+ /** Human clock, e.g. "14:05" or "昨天 20:31". */
31
+ export function formatTime(epochMs) {
32
+ const date = new Date(epochMs);
33
+ const clock = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
34
+ const today = new Date();
35
+ if (date.toDateString() === today.toDateString())
36
+ return clock;
37
+ const yesterday = new Date(today);
38
+ yesterday.setDate(today.getDate() - 1);
39
+ if (date.toDateString() === yesterday.toDateString())
40
+ return `昨天 ${clock}`;
41
+ return `${String(date.getMonth() + 1)}月${String(date.getDate())}日 ${clock}`;
42
+ }
43
+ /**
44
+ * The surface root.
45
+ * @returns the app tree.
46
+ */
47
+ export function App() {
48
+ const [route, setRoute] = useState({ kind: 'workspaces' });
49
+ const muxRef = useRef(undefined);
50
+ // The mux stream lives for the page lifetime: session events keep the
51
+ // open chat live, and reconnect is automatic.
52
+ useEffect(() => {
53
+ const mux = new MuxClient();
54
+ muxRef.current = mux;
55
+ mux.start();
56
+ return () => { mux.stop(); };
57
+ }, []);
58
+ const back = useCallback(() => {
59
+ setRoute(previous => {
60
+ if (previous.kind === 'chat')
61
+ return { kind: 'sessions', workspace: previous.workspace };
62
+ if (previous.kind === 'sessions')
63
+ return { kind: 'workspaces' };
64
+ return previous;
65
+ });
66
+ }, []);
67
+ const openChat = useCallback((session, workspace) => {
68
+ setRoute({ kind: 'chat', session, workspace });
69
+ }, []);
70
+ return (_jsx("div", { className: "mobile", children: route.kind === 'workspaces'
71
+ ? _jsx(WorkspaceRoster, { onPick: (workspace) => { setRoute({ kind: 'sessions', workspace }); } })
72
+ : route.kind === 'sessions'
73
+ ? (_jsx(SessionListView, { workspace: route.workspace, onBack: back, onPick: (session) => { openChat(session, route.workspace); } }))
74
+ : _jsx(ChatView, { session: route.session, mux: muxRef.current, onBack: back }) }));
75
+ }
76
+ /** Shared error text for the surface's small failure affordances. */
77
+ export function errorText(error) {
78
+ if (error instanceof RpcCallError)
79
+ return error.error.message;
80
+ if (error instanceof RpcTransportError)
81
+ return error.message;
82
+ if (error instanceof Error)
83
+ return error.message;
84
+ return String(error);
85
+ }
86
+ /**
87
+ * Actionable hint for transport-level 403s on host-gated channels (model
88
+ * picker, session creation): the phone's UI bundle is served fresh from disk
89
+ * per request, while the host-side allowlist lives in the long-running
90
+ * process — a rebuild without a restart shows the new surface against the
91
+ * old allowlist (HTTP 403 forbidden).
92
+ */
93
+ export function staleHostHint(message) {
94
+ return /^HTTP 403/.test(message)
95
+ ? '宿主端插件可能仍在运行旧版本:请重启 dsh web 后再试。'
96
+ : undefined;
97
+ }
98
+ /** Fetch one history page (tail by default) — thin wrapper so views share the call shape. */
99
+ export function loadHistory(sessionId, beforeSeq) {
100
+ return fetchHistory(sessionId, beforeSeq);
101
+ }
102
+ export { listSessions, listWorkspaces, prompt };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.js","sourceRoot":"","sources":["../../../../src/mobile/views/App.tsx"],"names":[],"mappings":"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"}