dsh-plugin-dshm 0.0.0-stage → 0.1.0

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 (80) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/README.zh-CN.md +190 -0
  5. package/cordis.patch.yml +7 -0
  6. package/lib/auth/signature.d.ts +4 -0
  7. package/lib/auth/signature.js +24 -0
  8. package/lib/cli/index.d.ts +4 -0
  9. package/lib/cli/index.js +41 -0
  10. package/lib/client.js +419 -0
  11. package/lib/config.d.ts +10 -0
  12. package/lib/config.js +27 -0
  13. package/lib/discovery/advertiser.d.ts +45 -0
  14. package/lib/discovery/advertiser.js +141 -0
  15. package/lib/discovery/interfaces.d.ts +5 -0
  16. package/lib/discovery/interfaces.js +24 -0
  17. package/lib/errors.d.ts +5 -0
  18. package/lib/errors.js +10 -0
  19. package/lib/host/local.d.ts +5 -0
  20. package/lib/host/local.js +122 -0
  21. package/lib/host/tailscale.d.ts +43 -0
  22. package/lib/host/tailscale.js +137 -0
  23. package/lib/host/types.d.ts +40 -0
  24. package/lib/host/types.js +1 -0
  25. package/lib/index.d.ts +30 -0
  26. package/lib/index.js +98 -0
  27. package/lib/methods/index.d.ts +9 -0
  28. package/lib/methods/index.js +206 -0
  29. package/lib/pairing/state.d.ts +26 -0
  30. package/lib/pairing/state.js +89 -0
  31. package/lib/pairing/window.d.ts +18 -0
  32. package/lib/pairing/window.js +57 -0
  33. package/lib/projection/client.d.mts +2 -0
  34. package/lib/projection/client.mjs +11 -0
  35. package/lib/projection/files.d.ts +33 -0
  36. package/lib/projection/files.js +278 -0
  37. package/lib/projection/fold-recording.d.mts +50 -0
  38. package/lib/projection/fold-recording.mjs +440 -0
  39. package/lib/projection/host.d.ts +6 -0
  40. package/lib/projection/host.js +86 -0
  41. package/lib/projection/ids.d.mts +1 -0
  42. package/lib/projection/ids.mjs +1 -0
  43. package/lib/projection/index.d.ts +2 -0
  44. package/lib/projection/index.js +2 -0
  45. package/lib/projection/interactions.d.ts +10 -0
  46. package/lib/projection/interactions.js +73 -0
  47. package/lib/projection/native.d.ts +114 -0
  48. package/lib/projection/native.js +903 -0
  49. package/lib/protocol/THIRD_PARTY_LICENSES.txt +135 -0
  50. package/lib/protocol/dmp.d.ts +402 -0
  51. package/lib/protocol/dmp.js +1 -0
  52. package/lib/protocol/schema/dmp-v1/common.schema.json +88 -0
  53. package/lib/protocol/schema/dmp-v1/events.schema.json +107 -0
  54. package/lib/protocol/schema/dmp-v1/frames.schema.json +280 -0
  55. package/lib/protocol/schema/dmp-v1/methods.schema.json +584 -0
  56. package/lib/protocol/schema/dmp-v1/models.schema.json +674 -0
  57. package/lib/protocol/scripts/contract.mjs +8106 -0
  58. package/lib/push/controller.d.ts +64 -0
  59. package/lib/push/controller.js +290 -0
  60. package/lib/push/observer.d.ts +51 -0
  61. package/lib/push/observer.js +266 -0
  62. package/lib/push/payload.d.ts +15 -0
  63. package/lib/push/payload.js +18 -0
  64. package/lib/push/store.d.ts +17 -0
  65. package/lib/push/store.js +75 -0
  66. package/lib/server/index.d.ts +30 -0
  67. package/lib/server/index.js +500 -0
  68. package/lib/server/rate-limit.d.ts +7 -0
  69. package/lib/server/rate-limit.js +19 -0
  70. package/lib/server/uploads.d.ts +21 -0
  71. package/lib/server/uploads.js +74 -0
  72. package/lib/types.d.ts +92 -0
  73. package/lib/types.js +1 -0
  74. package/lib/ui/card.d.mts +3 -0
  75. package/lib/ui/card.mjs +59 -0
  76. package/lib/ui/copy.d.mts +146 -0
  77. package/lib/ui/copy.mjs +106 -0
  78. package/lib/ui/remote-setup.d.mts +81 -0
  79. package/lib/ui/remote-setup.mjs +75 -0
  80. package/package.json +67 -4
@@ -0,0 +1,21 @@
1
+ import type { DmpObject } from '../types.js';
2
+ /** Connection-local upload buffers; no unverified bytes cross the projection boundary. */
3
+ export declare class Uploads {
4
+ private readonly allocate;
5
+ private readonly send;
6
+ private readonly commit;
7
+ private uploads;
8
+ private streams;
9
+ constructor(allocate: () => number, send: (frame: DmpObject) => unknown, commit: (sessionId: string, name: string, mime: string, bytes: Uint8Array, signal: AbortSignal) => Promise<{
10
+ attachmentId: string;
11
+ }>);
12
+ private touch;
13
+ private remove;
14
+ begin(params: DmpObject): DmpObject;
15
+ receive(frame: Buffer, maxChunkBytes: number): void;
16
+ finish(id: string, signal: AbortSignal): Promise<{
17
+ attachmentId: string;
18
+ }>;
19
+ abort(streamId: number): void;
20
+ dispose(): void;
21
+ }
@@ -0,0 +1,74 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { DmpError } from '../errors.js';
3
+ /** Connection-local upload buffers; no unverified bytes cross the projection boundary. */
4
+ export class Uploads {
5
+ allocate;
6
+ send;
7
+ commit;
8
+ uploads = new Map();
9
+ streams = new Map();
10
+ constructor(allocate, send, commit) {
11
+ this.allocate = allocate;
12
+ this.send = send;
13
+ this.commit = commit;
14
+ }
15
+ touch(upload) {
16
+ clearTimeout(upload.timer);
17
+ upload.timer = setTimeout(() => { this.remove(upload); this.send({ t: 'stream.abort', streamId: upload.streamId, code: 'timeout' }); }, 30000);
18
+ upload.timer.unref();
19
+ }
20
+ remove(upload) {
21
+ clearTimeout(upload.timer);
22
+ this.uploads.delete(upload.id);
23
+ this.streams.delete(upload.streamId);
24
+ }
25
+ begin(params) {
26
+ const limit = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(String(params.mime)) ? 20 * 1024 * 1024 : 32 * 1024 * 1024;
27
+ if (Number(params.size) > limit)
28
+ throw new DmpError('too-large', 'Attachment exceeds upload limit', { limit });
29
+ if (this.uploads.size >= 3)
30
+ throw new DmpError('busy');
31
+ const upload = { id: randomUUID(), streamId: this.allocate(), sessionId: String(params.sessionId), name: String(params.name), mime: String(params.mime),
32
+ size: Number(params.size), sha256: String(params.sha256), chunks: [], received: 0, final: false };
33
+ this.uploads.set(upload.id, upload);
34
+ this.streams.set(upload.streamId, upload);
35
+ this.touch(upload);
36
+ return { uploadId: upload.id, streamId: upload.streamId };
37
+ }
38
+ receive(frame, maxChunkBytes) {
39
+ if (frame.length < 5 || frame.length - 5 > maxChunkBytes || (frame[4] & ~1))
40
+ throw new DmpError('bad-frame');
41
+ const upload = this.streams.get(frame.readUInt32BE(0));
42
+ if (!upload || upload.final)
43
+ throw new DmpError('bad-frame');
44
+ const bytes = frame.subarray(5);
45
+ if (upload.received + bytes.length > upload.size)
46
+ throw new DmpError('bad-frame');
47
+ upload.chunks.push(bytes);
48
+ upload.received += bytes.length;
49
+ upload.final = frame[4] === 1;
50
+ this.touch(upload);
51
+ // Ack every chunk, which is stricter than the required 1 MiB cadence.
52
+ this.send({ t: 'stream.ack', streamId: upload.streamId, bytes: upload.received });
53
+ }
54
+ async finish(id, signal) {
55
+ const upload = this.uploads.get(id);
56
+ if (!upload)
57
+ throw new DmpError('not-found');
58
+ if (!upload.final)
59
+ throw new DmpError('invalid-params', 'Upload has no final frame');
60
+ this.remove(upload);
61
+ if (upload.received !== upload.size)
62
+ throw new DmpError('invalid-params', 'Upload size mismatch');
63
+ const bytes = Buffer.concat(upload.chunks);
64
+ if (createHash('sha256').update(bytes).digest('hex') !== upload.sha256)
65
+ throw new DmpError('invalid-params', 'Upload hash mismatch');
66
+ if (signal.aborted)
67
+ throw new DmpError('cancelled');
68
+ return this.commit(upload.sessionId, upload.name, upload.mime, bytes, signal);
69
+ }
70
+ abort(streamId) { const upload = this.streams.get(streamId); if (upload)
71
+ this.remove(upload); }
72
+ dispose() { for (const upload of this.uploads.values())
73
+ this.remove(upload); }
74
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { Capability, HostInfo, Device } from './protocol/dmp.js';
2
+ import type { PushMethods } from './push/controller.js';
3
+ export type DmpObject = Record<string, unknown>;
4
+ export interface DmpEvent {
5
+ kind: string;
6
+ data: DmpObject;
7
+ }
8
+ export type WatchMethod = 'sessions.watch' | 'session.watch' | 'jobs.watch' | 'job.watch';
9
+ export type ContentMethod = 'tool.output' | 'timeline.text' | 'files.download' | 'deliverables.download';
10
+ export interface ProjectionWatch {
11
+ result: DmpObject;
12
+ dispose(): void | Promise<void>;
13
+ }
14
+ export interface ProjectionPeer {
15
+ upload(sessionId: string, name: string, mime: string, bytes: Uint8Array, signal: AbortSignal): Promise<{
16
+ attachmentId: string;
17
+ }>;
18
+ create(workspaceId: string, sessionId: string, signal: AbortSignal): Promise<{
19
+ sessionId: string;
20
+ }>;
21
+ request(method: string, params: DmpObject, signal: AbortSignal): Promise<DmpObject>;
22
+ watch(method: WatchMethod, params: DmpObject, emit: (event: DmpEvent) => void, signal: AbortSignal): Promise<ProjectionWatch>;
23
+ content(method: ContentMethod, params: DmpObject, signal: AbortSignal): Promise<Uint8Array>;
24
+ dispose(): void | Promise<void>;
25
+ }
26
+ export interface ProjectionHost {
27
+ version: string;
28
+ status: 'ok' | 'incompatible' | 'unavailable';
29
+ connect(deviceId: string): Promise<ProjectionPeer>;
30
+ diagnostics?(): {
31
+ activePeers: number;
32
+ createdPeers: number;
33
+ disposedPeers: number;
34
+ };
35
+ dispose(): void | Promise<void>;
36
+ }
37
+ export type Reply = {
38
+ result: DmpObject;
39
+ } | {
40
+ bytes: Uint8Array;
41
+ mime?: string;
42
+ };
43
+ export interface ConnectionMethods {
44
+ upload?(sessionId: string, name: string, mime: string, bytes: Uint8Array, signal: AbortSignal): Promise<{
45
+ attachmentId: string;
46
+ }>;
47
+ request(method: string, params: DmpObject, signal: AbortSignal): Promise<Reply>;
48
+ dispose(): void | Promise<void>;
49
+ }
50
+ export interface ServerIdentity {
51
+ host: HostInfo;
52
+ version: string;
53
+ instanceId: string;
54
+ dshVersion: string;
55
+ dshStatus: ProjectionHost['status'];
56
+ caps: Capability[];
57
+ }
58
+ export interface DeviceService {
59
+ list(): Device[];
60
+ rename(deviceId: string, name: string): Device;
61
+ revoke(deviceId: string): void;
62
+ watch(changed: () => void): () => void;
63
+ }
64
+ export interface MethodContext {
65
+ push?: PushMethods;
66
+ uploads: {
67
+ begin(params: DmpObject): DmpObject;
68
+ finish(uploadId: string, signal: AbortSignal): Promise<{
69
+ attachmentId: string;
70
+ }>;
71
+ };
72
+ devices: DeviceService;
73
+ deviceId: string;
74
+ identity: ServerIdentity;
75
+ emit(sub: string, event: DmpEvent): void;
76
+ }
77
+ export interface BridgeServerOptions {
78
+ onAuthenticated?(deviceId: string): void;
79
+ onRevoked?(deviceId: string): void;
80
+ onAddresses?(addresses: string[], port: number): Promise<void> | void;
81
+ stateDir: string;
82
+ port: number;
83
+ addresses?: string[];
84
+ bindMode?: 'lan' | 'tailscale' | 'all-private';
85
+ pairing?: boolean;
86
+ quiet?: boolean;
87
+ version: string;
88
+ dshVersion: string;
89
+ dshStatus: ProjectionHost['status'];
90
+ caps: Capability[];
91
+ connect(context: MethodContext): Promise<ConnectionMethods>;
92
+ }
package/lib/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export function createSettingsCard(React: any, { Button }: {
2
+ Button: any;
3
+ }, apiBase: any): () => any;
@@ -0,0 +1,59 @@
1
+ // DMP-shaped host controls; all native module/slot knowledge stays in projection/.
2
+ import { useCopy, guidanceCopy } from './copy.mjs';
3
+ import { createRemoteSetup } from './remote-setup.mjs';
4
+ export function createSettingsCard(React, { Button }, apiBase) {
5
+ const h = React.createElement;
6
+ const RemoteSetup = createRemoteSetup(React, { Button });
7
+ const icon = (kind) => h('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, 'aria-hidden': true }, h('path', { d: kind === 'pair' ? 'M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h2v2h-2zM18 14h2v6h-6v-2' : 'M5 7h14M9 7V4h6v3M7 7l1 13h8l1-13M10 10v7M14 10v7' }));
8
+ const style = `.dshm-card{color:var(--dsw-alias-label-primary);font:inherit;font-size:16px;line-height:1.5;display:grid;gap:24px}.dshm-card h2,.dshm-card h3,.dshm-card p{margin:0}.dshm-card h2{font-size:20px}.dshm-card h3{font-size:16px}.dshm-card button{min-height:44px;gap:8px}.dshm-card button:focus-visible,.dshm-link-button:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.dshm-row{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}.dshm-muted{color:var(--dsw-alias-label-secondary);font-size:14px}.dshm-panel{padding:16px;border:1px solid var(--dsw-alias-border-primary,currentColor);border-radius:var(--dsw-radius-md,12px);display:grid;gap:12px}.dshm-qr{display:block;width:256px;max-width:100%;height:auto;background:white;border-radius:8px}.dshm-devices{list-style:none;margin:0;padding:0;display:grid;gap:12px}.dshm-device{padding:14px 0;border-bottom:1px solid var(--dsw-alias-border-primary,currentColor)}.dshm-actions{display:flex;gap:8px;flex-wrap:wrap}.dshm-setup-steps{margin:0;padding-left:24px;display:grid;gap:24px}.dshm-setup-steps li{display:grid;gap:12px;min-width:0}.dshm-link-button{display:inline-flex;align-items:center;min-height:44px;padding:8px 12px;border:1px solid var(--dsw-alias-border-primary,currentColor);border-radius:var(--dsw-radius-md,12px);color:inherit;text-decoration:underline;box-sizing:border-box}.dshm-error{color:var(--dsw-alias-danger-label,#b42318)}.dshm-guidance{padding:14px 16px;background:var(--dsw-alias-bg-layer-3);border-radius:var(--dsw-radius-md,12px)}@media(max-width:480px){.dshm-row{align-items:flex-start}.dshm-qr{width:224px}}`;
9
+ return function SettingsCard() {
10
+ const { language, copy } = useCopy(React);
11
+ const [status, setStatus] = React.useState(null), [devices, setDevices] = React.useState([]);
12
+ const [pairing, setPairing] = React.useState(null), [busy, setBusy] = React.useState(false);
13
+ const [error, setError] = React.useState(''), [confirm, setConfirm] = React.useState(null), [now, setNow] = React.useState(Date.now());
14
+ const [remoteOpen, setRemoteOpen] = React.useState(false);
15
+ const pairPanel = React.useRef(null);
16
+ React.useEffect(() => { if (pairing)
17
+ pairPanel.current?.focus(); }, [pairing?.expiresAt]);
18
+ const api = async (route, body) => {
19
+ const response = await fetch(`${apiBase}/${route}`, { method: body === undefined ? 'GET' : 'POST', credentials: 'same-origin', cache: 'no-store', headers: body === undefined ? {} : { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(6000) });
20
+ if (!response.ok)
21
+ throw new Error(route === 'pairRemote' && response.status === 409 ? '409' : 'requestError');
22
+ return response.json();
23
+ };
24
+ const refresh = async () => { const [next, list] = await Promise.all([api('status'), api('devices')]); setStatus(next); setDevices(list.devices); if (!next.pairing.active)
25
+ setPairing(null); };
26
+ React.useEffect(() => {
27
+ let live = true;
28
+ const poll = async () => { try {
29
+ const [next, list] = await Promise.all([api('status'), api('devices')]);
30
+ if (live) {
31
+ setStatus(next);
32
+ setDevices(list.devices);
33
+ if (!next.pairing.active)
34
+ setPairing(null);
35
+ }
36
+ }
37
+ catch {
38
+ if (live)
39
+ setError('bridgeError');
40
+ } };
41
+ void poll();
42
+ const timer = setInterval(poll, 2000), clock = setInterval(() => setNow(Date.now()), 1000);
43
+ return () => { live = false; clearInterval(timer); clearInterval(clock); };
44
+ }, []);
45
+ const action = async (work) => { setBusy(true); setError(''); try {
46
+ await work();
47
+ await refresh();
48
+ }
49
+ catch (failure) {
50
+ setError(failure.message);
51
+ }
52
+ finally {
53
+ setBusy(false);
54
+ } };
55
+ const seconds = pairing ? Math.max(0, Math.ceil((Date.parse(pairing.expiresAt) - now) / 1000)) : 0;
56
+ const button = (label, onClick, variant = 'outline', glyph) => h(Button, { variant, disabled: busy, onClick }, glyph ? icon(glyph) : null, label);
57
+ return h('section', { className: 'dshm-card', 'aria-label': copy.cardLabel, lang: language }, h('style', null, style), h('div', { className: 'dshm-row' }, h('div', null, h('h2', null, 'DSHM'), h('p', { className: 'dshm-muted' }, status ? `${copy.connectedCount(status.connectedDevices)} · ${status.addresses.length ? copy.listening : copy.noNetwork}` : copy.loading)), button(busy ? copy.working : copy.pair, () => action(async () => { const next = await api('pair', {}); setNow(Date.now()); setPairing(next); }), 'primary', 'pair')), error ? h('div', { className: 'dshm-panel', role: 'alert' }, h('p', { className: 'dshm-error' }, copy[error] ?? copy.requestError), button(copy.retry, () => action(refresh))) : null, pairing && seconds > 0 ? h('div', { className: 'dshm-panel', ref: pairPanel, tabIndex: -1, 'aria-label': copy.qrLabel }, h('h3', null, copy.scan), h('img', { className: 'dshm-qr', src: `data:image/svg+xml;base64,${btoa(pairing.svg)}`, alt: copy.qrAlt }), h('p', { role: 'timer', 'aria-live': 'off' }, copy.expires(`${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`)), button(copy.cancelPair, () => action(async () => { await api('cancelPair', {}); setPairing(null); }))) : null, ...(status?.guidance ?? []).map(text => h('p', { className: 'dshm-guidance', key: text }, guidanceCopy(text, copy))), status?.discovery.state === 'unavailable' ? h('p', { role: 'status' }, copy.bonjour) : null, h('div', { className: 'dshm-row' }, h('h3', null, copy.away), h(Button, { variant: 'outline', 'aria-expanded': remoteOpen, 'aria-controls': 'dshm-remote-setup', onClick: () => setRemoteOpen(!remoteOpen) }, remoteOpen ? copy.closeSetup : copy.openSetup)), remoteOpen ? h(RemoteSetup, { api, copy, pairingBusy: busy, onPair: () => action(async () => { const next = await api('pairRemote', {}); setNow(Date.now()); setPairing(next); }) }) : null, h('div', null, h('h3', null, copy.devices), devices.length ? h('ul', { className: 'dshm-devices' }, ...devices.map(device => h('li', { className: 'dshm-device', key: device.deviceId }, h('div', { className: 'dshm-row' }, h('div', null, h('strong', null, device.name), h('p', { className: 'dshm-muted' }, `${device.connected ? device.connectedVia === 'tailscale' ? copy.viaTailscale : device.connectedVia === 'lan' ? copy.viaLAN : copy.connected : copy.offline} · ${device.lastSeenAt ? copy.lastSeen(new Date(device.lastSeenAt).toLocaleString(language)) : copy.notConnected}`)), confirm === device.deviceId ? h('div', null, h('p', null, copy.revokeQuestion(device.name)), h('div', { className: 'dshm-actions' }, button(copy.cancel, () => setConfirm(null)), button(copy.revokeDevice, () => action(async () => { await api('revoke', { deviceId: device.deviceId }); setConfirm(null); }), 'outline', 'revoke'))) : button(copy.revoke, () => setConfirm(device.deviceId), 'outline', 'revoke'))))) : h('p', { className: 'dshm-muted' }, copy.noDevices)));
58
+ };
59
+ }
@@ -0,0 +1,146 @@
1
+ export function languageFor(hostLanguage: any, browserLanguage: any): "zh-CN" | "en";
2
+ export function useCopy(React: any): {
3
+ language: any;
4
+ copy: any;
5
+ };
6
+ export function guidanceCopy(text: any, copy: any): any;
7
+ export const copies: {
8
+ en: {
9
+ intro: string;
10
+ step1: string;
11
+ step2: string;
12
+ 'step2.detail': string;
13
+ step3: string;
14
+ 'step3.detail': string;
15
+ 'step3.check': string;
16
+ 'button.pairRemote': string;
17
+ 'button.open': string;
18
+ 'button.check': string;
19
+ away: string;
20
+ states: {
21
+ 'not-installed': string[];
22
+ 'needs-login': string[];
23
+ 'needs-approval': string[];
24
+ stopped: string[];
25
+ starting: string[];
26
+ offline: string[];
27
+ unavailable: string[];
28
+ unsupported: string[];
29
+ 'bridge-not-ready': string[];
30
+ ready: string[];
31
+ connected: string[];
32
+ };
33
+ '409': string;
34
+ requestError: string;
35
+ bridgeError: string;
36
+ setupError: string;
37
+ openError: string;
38
+ checkingTailscale: string;
39
+ unavailableStatus: string;
40
+ opening: string;
41
+ checking: string;
42
+ getComputer: string;
43
+ getPhone: string;
44
+ installDetail: string;
45
+ opened: string;
46
+ cardLabel: string;
47
+ connectedCount: (count: any) => string;
48
+ listening: string;
49
+ noNetwork: string;
50
+ loading: string;
51
+ working: string;
52
+ pair: string;
53
+ retry: string;
54
+ qrLabel: string;
55
+ scan: string;
56
+ qrAlt: string;
57
+ expires: (time: any) => string;
58
+ cancelPair: string;
59
+ guidanceMac: string;
60
+ guidanceWindows: string;
61
+ bonjour: string;
62
+ closeSetup: string;
63
+ openSetup: string;
64
+ devices: string;
65
+ viaTailscale: string;
66
+ viaLAN: string;
67
+ connected: string;
68
+ offline: string;
69
+ lastSeen: (date: any) => string;
70
+ notConnected: string;
71
+ revokeQuestion: (name: any) => string;
72
+ cancel: string;
73
+ revokeDevice: string;
74
+ revoke: string;
75
+ noDevices: string;
76
+ };
77
+ 'zh-CN': {
78
+ intro: string;
79
+ step1: string;
80
+ step2: string;
81
+ 'step2.detail': string;
82
+ step3: string;
83
+ 'step3.detail': string;
84
+ 'step3.check': string;
85
+ 'button.pairRemote': string;
86
+ 'button.open': string;
87
+ 'button.check': string;
88
+ away: string;
89
+ states: {
90
+ 'not-installed': string[];
91
+ 'needs-login': string[];
92
+ 'needs-approval': string[];
93
+ stopped: string[];
94
+ starting: string[];
95
+ offline: string[];
96
+ unavailable: string[];
97
+ unsupported: string[];
98
+ 'bridge-not-ready': string[];
99
+ ready: string[];
100
+ connected: string[];
101
+ };
102
+ '409': string;
103
+ requestError: string;
104
+ bridgeError: string;
105
+ setupError: string;
106
+ openError: string;
107
+ checkingTailscale: string;
108
+ unavailableStatus: string;
109
+ opening: string;
110
+ checking: string;
111
+ getComputer: string;
112
+ getPhone: string;
113
+ installDetail: string;
114
+ opened: string;
115
+ cardLabel: string;
116
+ connectedCount: (count: any) => string;
117
+ listening: string;
118
+ noNetwork: string;
119
+ loading: string;
120
+ working: string;
121
+ pair: string;
122
+ retry: string;
123
+ qrLabel: string;
124
+ scan: string;
125
+ qrAlt: string;
126
+ expires: (time: any) => string;
127
+ cancelPair: string;
128
+ guidanceMac: string;
129
+ guidanceWindows: string;
130
+ bonjour: string;
131
+ closeSetup: string;
132
+ openSetup: string;
133
+ devices: string;
134
+ viaTailscale: string;
135
+ viaLAN: string;
136
+ connected: string;
137
+ offline: string;
138
+ lastSeen: (date: any) => string;
139
+ notConnected: string;
140
+ revokeQuestion: (name: any) => string;
141
+ cancel: string;
142
+ revokeDevice: string;
143
+ revoke: string;
144
+ noDevices: string;
145
+ };
146
+ };
@@ -0,0 +1,106 @@
1
+ // DSH publishes its active locale on <html lang>; other carriers use the browser language.
2
+ export function languageFor(hostLanguage, browserLanguage) {
3
+ const language = typeof hostLanguage === 'string' && hostLanguage.trim() ? hostLanguage : browserLanguage;
4
+ return /^zh(?:[-_]|$)/i.test(language ?? '') ? 'zh-CN' : 'en';
5
+ }
6
+ const readLanguage = () => languageFor(globalThis.document?.documentElement.lang, globalThis.navigator?.language);
7
+ function subscribeLanguage(listener) {
8
+ const element = globalThis.document?.documentElement;
9
+ const observer = element && typeof MutationObserver !== 'undefined' ? new MutationObserver(listener) : undefined;
10
+ observer?.observe(element, { attributes: true, attributeFilter: ['lang'] });
11
+ globalThis.addEventListener?.('languagechange', listener);
12
+ return () => { observer?.disconnect(); globalThis.removeEventListener?.('languagechange', listener); };
13
+ }
14
+ export function useCopy(React) {
15
+ const language = React.useSyncExternalStore(subscribeLanguage, readLanguage, () => 'en');
16
+ return { language, copy: copies[language] };
17
+ }
18
+ export const copies = {
19
+ en: {
20
+ intro: 'Phone not on the same Wi-Fi as this computer? Use Tailscale. On the same Wi-Fi, nothing else is needed.',
21
+ step1: 'Connect this computer', step2: 'Connect your iPhone or iPad',
22
+ 'step2.detail': 'Install Tailscale, sign in to the same account, and connect.',
23
+ step3: 'Pair with a fresh code',
24
+ 'step3.detail': 'Connect Tailscale on both devices first. Paired on Wi-Fi before? Forget the computer in DSHM, revoke the old device below, then pair here.',
25
+ 'step3.check': 'Test with your phone on cellular or a different Wi-Fi.',
26
+ 'button.pairRemote': 'Pair for remote access', 'button.open': 'Open Tailscale', 'button.check': 'Check again', away: 'Not on the same Wi-Fi',
27
+ states: {
28
+ 'not-installed': ['Install Tailscale on this computer', 'Use the official installer, then check again.'],
29
+ 'needs-login': ['Sign in to Tailscale', 'Open Tailscale and finish signing in.'],
30
+ 'needs-approval': ['Waiting for approval', 'Ask your Tailscale admin to approve this computer.'],
31
+ stopped: ['Tailscale is disconnected', 'Open Tailscale and choose Connect.'],
32
+ starting: ['Tailscale is starting', 'Check again in a moment.'],
33
+ offline: ['Tailscale is offline', "Check this computer's internet connection."],
34
+ unavailable: ["Couldn't check Tailscale", 'Open Tailscale, then check again.'],
35
+ unsupported: ['Set up your VPN manually', 'Automatic checks support Windows and macOS. Same-network pairing still works.'],
36
+ 'bridge-not-ready': ['Tailscale connected, DSHM not ready yet', 'Check again in a few seconds. If it persists, check the firewall.'],
37
+ ready: ['Ready for remote pairing', 'No phone has connected over Tailscale yet.'],
38
+ connected: ['Phone connected over Tailscale', ''],
39
+ },
40
+ '409': 'Connect Tailscale on this computer, check again, then make a new code.',
41
+ requestError: 'DSHM could not complete this request. Try again.',
42
+ bridgeError: 'The bridge is unavailable. Check the host, then retry.',
43
+ setupError: 'Could not check remote setup. Check the host and retry. Local pairing is still available.',
44
+ openError: 'Could not open Tailscale on the host. Open it from the Start menu or Applications, then check again.',
45
+ checkingTailscale: 'Checking Tailscale…', unavailableStatus: 'Status unavailable', opening: 'Opening…', checking: 'Checking…',
46
+ getComputer: 'Get Tailscale for this computer', getPhone: 'Get Tailscale for iPhone or iPad',
47
+ installDetail: 'Run the installer on the computer running DSH. Complete installation and sign-in yourself; canceling leaves local pairing available.',
48
+ opened: 'Tailscale was opened on the host. Complete sign-in or choose Connect there, then return here.',
49
+ cardLabel: 'DSHM remote access', connectedCount: count => `${count} connected`, listening: 'Listening', noNetwork: 'No private network', loading: 'Loading bridge…',
50
+ working: 'Working…', pair: 'Pair a device', retry: 'Retry', qrLabel: 'Pairing QR code', scan: 'Scan with your iPhone', qrAlt: 'DSHM one-use pairing QR code',
51
+ expires: time => `Expires in ${time}`, cancelPair: 'Cancel pairing',
52
+ guidanceMac: 'Allow DeepSeek Harness under System Settings → Privacy & Security → Local Network. If you run dsh web in a terminal, allow that terminal too.',
53
+ guidanceWindows: 'Allow DeepSeek Harness on Private networks when Windows Firewall asks. Keep Public networks disabled.',
54
+ bonjour: 'Bonjour is unavailable on this network. Pairing still includes the host’s direct addresses.',
55
+ closeSetup: 'Close remote setup', openSetup: 'Set up remote access', devices: 'Paired devices',
56
+ viaTailscale: 'Connected via Tailscale', viaLAN: 'Connected via local network', connected: 'Connected', offline: 'Offline',
57
+ lastSeen: date => `Last seen ${date}`, notConnected: 'Not connected yet', revokeQuestion: name => `Revoke ${name}?`,
58
+ cancel: 'Cancel', revokeDevice: 'Revoke device', revoke: 'Revoke', noDevices: 'No paired devices.',
59
+ },
60
+ 'zh-CN': {
61
+ intro: '手机和这台电脑不在同一 Wi-Fi?使用 Tailscale。在同一 Wi-Fi 下无需其他设置。',
62
+ step1: '连接这台电脑', step2: '连接 iPhone 或 iPad',
63
+ 'step2.detail': '安装 Tailscale,登录同一账户并连接。',
64
+ step3: '用新的配对码配对',
65
+ 'step3.detail': '请先在两台设备上连接 Tailscale。之前在 Wi-Fi 下配对过?在 DSHM 中移除这台电脑,在下方撤销旧设备,然后在此重新配对。',
66
+ 'step3.check': '请让手机使用蜂窝网络或其他 Wi-Fi 进行测试。',
67
+ 'button.pairRemote': '配对以远程访问', 'button.open': '打开 Tailscale', 'button.check': '再次检查', away: '不在同一 Wi-Fi',
68
+ states: {
69
+ 'not-installed': ['在这台电脑上安装 Tailscale', '使用官方安装程序,然后再次检查。'],
70
+ 'needs-login': ['登录 Tailscale', '打开 Tailscale 并完成登录。'],
71
+ 'needs-approval': ['等待批准', '请 Tailscale 管理员批准这台电脑。'],
72
+ stopped: ['Tailscale 未连接', '打开 Tailscale 并选择“连接”。'],
73
+ starting: ['Tailscale 正在启动', '请稍后再次检查。'],
74
+ offline: ['Tailscale 离线', '请检查这台电脑的网络连接。'],
75
+ unavailable: ['无法检查 Tailscale', '打开 Tailscale 后再次检查。'],
76
+ unsupported: ['请手动设置 VPN', '自动检查仅支持 Windows 和 macOS。同一网络配对仍可使用。'],
77
+ 'bridge-not-ready': ['Tailscale 已连接,DSHM 尚未就绪', '请几秒后再次检查。如持续出现,请检查防火墙。'],
78
+ ready: ['可以远程配对', '尚无手机通过 Tailscale 连接。'],
79
+ connected: ['手机已通过 Tailscale 连接', ''],
80
+ },
81
+ '409': '请在这台电脑上连接 Tailscale,再次检查后生成新的配对码。',
82
+ requestError: 'DSHM 无法完成此请求,请重试。', bridgeError: '桥接不可用,请检查电脑后重试。',
83
+ setupError: '无法检查远程设置,请检查电脑后重试。局域网配对仍可使用。',
84
+ openError: '无法在电脑上打开 Tailscale。请从“开始”菜单或“应用程序”中打开,然后再次检查。',
85
+ checkingTailscale: '正在检查 Tailscale…', unavailableStatus: '状态不可用', opening: '正在打开…', checking: '正在检查…',
86
+ getComputer: '获取电脑版 Tailscale', getPhone: '获取 iPhone 或 iPad 版 Tailscale',
87
+ installDetail: '请在运行 DSH 的电脑上安装,并亲自完成安装和登录。取消后仍可在局域网配对。',
88
+ opened: '已在电脑上打开 Tailscale。请完成登录或选择“连接”,然后返回此处。',
89
+ cardLabel: 'DSHM 远程访问', connectedCount: count => `${count} 台设备已连接`, listening: '正在监听', noNetwork: '没有私有网络', loading: '正在加载桥接…',
90
+ working: '正在处理…', pair: '配对设备', retry: '重试', qrLabel: '配对二维码', scan: '用 iPhone 扫描', qrAlt: 'DSHM 一次性配对二维码',
91
+ expires: time => `${time} 后过期`, cancelPair: '取消配对',
92
+ guidanceMac: '请在“系统设置 → 隐私与安全性 → 本地网络”中允许 DeepSeek Harness。如果在终端运行 dsh web,也请允许该终端。',
93
+ guidanceWindows: 'Windows 防火墙询问时,请允许 DeepSeek Harness 访问专用网络,并保持公用网络禁用。',
94
+ bonjour: '此网络上的 Bonjour 不可用,配对仍包含电脑的直接连接地址。',
95
+ closeSetup: '关闭远程设置', openSetup: '设置远程访问', devices: '已配对设备',
96
+ viaTailscale: '已通过 Tailscale 连接', viaLAN: '已通过局域网连接', connected: '已连接', offline: '离线',
97
+ lastSeen: date => `上次连接:${date}`, notConnected: '尚未连接', revokeQuestion: name => `撤销 ${name} 的访问权限?`,
98
+ cancel: '取消', revokeDevice: '撤销设备', revoke: '撤销', noDevices: '没有已配对设备。',
99
+ },
100
+ };
101
+ export function guidanceCopy(text, copy) {
102
+ for (const key of ['guidanceMac', 'guidanceWindows'])
103
+ if (text === copies.en[key])
104
+ return copy[key];
105
+ return text;
106
+ }
@@ -0,0 +1,81 @@
1
+ export function setupPresentation(status: any, copy?: {
2
+ intro: string;
3
+ step1: string;
4
+ step2: string;
5
+ 'step2.detail': string;
6
+ step3: string;
7
+ 'step3.detail': string;
8
+ 'step3.check': string;
9
+ 'button.pairRemote': string;
10
+ 'button.open': string;
11
+ 'button.check': string;
12
+ away: string;
13
+ states: {
14
+ 'not-installed': string[];
15
+ 'needs-login': string[];
16
+ 'needs-approval': string[];
17
+ stopped: string[];
18
+ starting: string[];
19
+ offline: string[];
20
+ unavailable: string[];
21
+ unsupported: string[];
22
+ 'bridge-not-ready': string[];
23
+ ready: string[];
24
+ connected: string[];
25
+ };
26
+ '409': string;
27
+ requestError: string;
28
+ bridgeError: string;
29
+ setupError: string;
30
+ openError: string;
31
+ checkingTailscale: string;
32
+ unavailableStatus: string;
33
+ opening: string;
34
+ checking: string;
35
+ getComputer: string;
36
+ getPhone: string;
37
+ installDetail: string;
38
+ opened: string;
39
+ cardLabel: string;
40
+ connectedCount: (count: any) => string;
41
+ listening: string;
42
+ noNetwork: string;
43
+ loading: string;
44
+ working: string;
45
+ pair: string;
46
+ retry: string;
47
+ qrLabel: string;
48
+ scan: string;
49
+ qrAlt: string;
50
+ expires: (time: any) => string;
51
+ cancelPair: string;
52
+ guidanceMac: string;
53
+ guidanceWindows: string;
54
+ bonjour: string;
55
+ closeSetup: string;
56
+ openSetup: string;
57
+ devices: string;
58
+ viaTailscale: string;
59
+ viaLAN: string;
60
+ connected: string;
61
+ offline: string;
62
+ lastSeen: (date: any) => string;
63
+ notConnected: string;
64
+ revokeQuestion: (name: any) => string;
65
+ cancel: string;
66
+ revokeDevice: string;
67
+ revoke: string;
68
+ noDevices: string;
69
+ }): {
70
+ title: any;
71
+ detail: any;
72
+ ready: boolean;
73
+ };
74
+ export function createRemoteSetup(React: any, { Button }: {
75
+ Button: any;
76
+ }): ({ api, onPair, pairingBusy, copy }: {
77
+ api: any;
78
+ onPair: any;
79
+ pairingBusy: any;
80
+ copy: any;
81
+ }) => any;