linkshell-cli 0.10.49 → 0.10.51

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.
@@ -1,5 +1,6 @@
1
1
  export { defaultHome, startHost, hostPaths, isHostRunning, type HostOptions, type HostPaths, type RunningHost } from "./host.js";
2
2
  export { connectHost, type HostClient } from "./rpc/client.js";
3
+ export { localWebSocket, windowsPipe } from "./local-transport.js";
3
4
  export { SessionHub, type Subscriber } from "./hub.js";
4
5
  export { HostStore } from "./store.js";
5
6
  export { CodexDriver } from "./drivers/codex/driver.js";
@@ -1,5 +1,6 @@
1
1
  export { defaultHome, startHost, hostPaths, isHostRunning } from "./host.js";
2
2
  export { connectHost } from "./rpc/client.js";
3
+ export { localWebSocket, windowsPipe } from "./local-transport.js";
3
4
  export { SessionHub } from "./hub.js";
4
5
  export { HostStore } from "./store.js";
5
6
  export { CodexDriver } from "./drivers/codex/driver.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../host/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAsD,MAAM,WAAW,CAAC;AACjI,OAAO,EAAE,WAAW,EAAmB,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAmB,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAGxD,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAqB,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEnE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../host/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAsD,MAAM,WAAW,CAAC;AACjI,OAAO,EAAE,WAAW,EAAmB,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,UAAU,EAAmB,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAGxD,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAqB,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEnE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
@@ -2,4 +2,6 @@ import WebSocket from "ws";
2
2
  export declare const isNamedPipe: (path: string) => boolean;
3
3
  /** Stable across callers, separate for each state directory, without the Unix path limit. */
4
4
  export declare function windowsPipe(home: string, name: string): string;
5
- export declare function localWebSocket(target: string, timeout?: number): WebSocket;
5
+ export declare function localWebSocket(target: string, timeout?: number, limits?: {
6
+ maxPayload?: number;
7
+ }): WebSocket;
@@ -9,14 +9,19 @@ export function windowsPipe(home, name) {
9
9
  const tag = createHash("sha256").update(key).digest("hex").slice(0, 24);
10
10
  return `\\\\.\\pipe\\linkshell-${tag}-${name}`;
11
11
  }
12
- export function localWebSocket(target, timeout = 10_000) {
13
- const options = { perMessageDeflate: false, handshakeTimeout: timeout };
12
+ export function localWebSocket(target, timeout = 10_000, limits = {}) {
13
+ const options = { perMessageDeflate: false, handshakeTimeout: timeout, ...limits };
14
14
  // URL parsers treat Windows backslashes and drive letters as URL syntax. Connect directly.
15
- if (isNamedPipe(target))
16
- return new WebSocket("ws://localhost/", {
17
- ...options,
18
- createConnection: () => connect(target),
19
- });
20
- return new WebSocket(/^wss?:\/\//.test(target) ? target : `ws+unix://${target}:/`, options);
15
+ const socket = isNamedPipe(target) ? new WebSocket("ws://localhost/", {
16
+ ...options,
17
+ createConnection: () => connect(target),
18
+ }) : new WebSocket(/^wss?:\/\//.test(target) ? target : `ws+unix://${target}:/`, options);
19
+ // Node's HTTP request timeout does not reliably fire on Windows pipe handshakes.
20
+ const deadline = setTimeout(() => socket.terminate(), timeout);
21
+ deadline.unref();
22
+ socket.once("open", () => clearTimeout(deadline));
23
+ socket.once("close", () => clearTimeout(deadline));
24
+ socket.once("error", () => clearTimeout(deadline));
25
+ return socket;
21
26
  }
22
27
  //# sourceMappingURL=local-transport.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"local-transport.js","sourceRoot":"","sources":["../../../../host/src/local-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,SAAS,MAAM,IAAI,CAAC;AAE3B,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC;AAEvF,6FAA6F;AAC7F,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,IAAY;IACpD,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACrE,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACxE,OAAO,0BAA0B,GAAG,IAAI,IAAI,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,OAAO,GAAG,MAAM;IAC7D,MAAM,OAAO,GAAG,EAAE,iBAAiB,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC;IACxE,2FAA2F;IAC3F,IAAI,WAAW,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,SAAS,CAAC,iBAAiB,EAAE;YAC/D,GAAG,OAAO;YACV,gBAAgB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;SACxC,CAAC,CAAC;IACH,OAAO,IAAI,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,MAAM,IAAI,EAAE,OAAO,CAAC,CAAC;AAC9F,CAAC"}
1
+ {"version":3,"file":"local-transport.js","sourceRoot":"","sources":["../../../../host/src/local-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,SAAS,MAAM,IAAI,CAAC;AAE3B,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC;AAEvF,6FAA6F;AAC7F,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,IAAY;IACpD,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACrE,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACxE,OAAO,0BAA0B,GAAG,IAAI,IAAI,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,OAAO,GAAG,MAAM,EAAE,SAAkC,EAAE;IACnG,MAAM,OAAO,GAAG,EAAE,iBAAiB,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC;IACnF,2FAA2F;IAC3F,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,iBAAiB,EAAE;QACpE,GAAG,OAAO;QACV,gBAAgB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;KACxC,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,MAAM,IAAI,EAAE,OAAO,CAAC,CAAC;IAC1F,iFAAiF;IACjF,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,OAAO,CAAC,CAAC;IAC/D,QAAQ,CAAC,KAAK,EAAE,CAAC;IACjB,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClD,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;IACnD,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;IACnD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare const SCREEN_LOADING_STYLE: string;
2
+ export declare const SCREEN_LOADING_HTML: string;
3
+ export declare const SCREEN_LOADING_SCRIPT: string;
@@ -0,0 +1,93 @@
1
+ // Shared by the browser viewer and the phone's WebView, including native video reception.
2
+ export const SCREEN_LOADING_STYLE = String.raw `
3
+ #note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
4
+ padding: max(28px, env(safe-area-inset-top)) 24px max(88px, env(safe-area-inset-bottom));
5
+ color: #f4f4f7; text-align: center; pointer-events: none; background: rgba(8,9,14,.94); }
6
+ .screen-loading-card { width: min(100%, 340px); }
7
+ .screen-loading-art { position: relative; width: 104px; height: 84px; margin: 0 auto 28px; color: #a5a9ef; }
8
+ .screen-loading-art::before { content: ''; position: absolute; inset: -32px; border-radius: 50%;
9
+ background: radial-gradient(ellipse, rgba(129,140,248,.16), transparent 68%); animation: screen-breathe 2.8s ease-in-out infinite; }
10
+ .screen-loading-monitor { position: absolute; inset: 6px 4px 17px; border: 1px solid rgba(165,169,239,.5); border-radius: 10px;
11
+ background: rgba(129,140,248,.05); overflow: hidden; }
12
+ .screen-loading-monitor::before { content: ''; position: absolute; top: 12px; left: 12px; width: 23px; height: 4px;
13
+ border-radius: 2px; background: rgba(165,169,239,.4); box-shadow: 0 11px 0 rgba(165,169,239,.12), 0 22px 0 rgba(165,169,239,.12); }
14
+ .screen-loading-monitor::after { content: ''; position: absolute; top: 0; bottom: 0; left: -45%; width: 45%;
15
+ background: linear-gradient(90deg, transparent, rgba(165,169,239,.16), transparent); animation: screen-scan 2.4s ease-in-out infinite; }
16
+ .screen-loading-stand { position: absolute; bottom: 8px; left: 50%; width: 26px; height: 10px; transform: translateX(-50%);
17
+ border-bottom: 1px solid rgba(165,169,239,.5); }
18
+ .screen-loading-stand::before { content: ''; position: absolute; left: 50%; height: 9px; border-left: 1px solid rgba(165,169,239,.5); }
19
+ #screen-loading-title { margin: 0; font-size: calc(18px * var(--text-scale, 1)); font-weight: 600; letter-spacing: -.3px; }
20
+ #screen-loading-detail { margin: 10px 0 0; color: rgba(255,255,255,.52); font-size: calc(13px * var(--text-scale, 1)); line-height: 1.7; overflow-wrap: anywhere; }
21
+ .screen-loading-steps { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 24px;
22
+ color: rgba(255,255,255,.3); font-size: calc(11px * var(--text-scale, 1)); }
23
+ .screen-loading-steps i { width: 24px; height: 1px; background: rgba(255,255,255,.12); }
24
+ .screen-loading-steps .current { color: #bbbffb; }
25
+ .screen-loading-steps .complete { color: rgba(255,255,255,.68); }
26
+ #note[data-state="error"] .screen-loading-steps { display: none; }
27
+ #screen-loading-retry { pointer-events: auto; margin-top: 22px; min-height: 44px; padding: 0 20px; border-radius: 10px;
28
+ border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.07); color: #eeeef4; font: inherit; font-size: 13px; cursor: pointer; }
29
+ #screen-loading-retry:hover { background: rgba(255,255,255,.13); }
30
+ #screen-loading-retry:focus-visible { outline: 2px solid #a5a9ef; outline-offset: 4px; }
31
+ #note[data-state="error"] .screen-loading-art::before, #note[data-state="error"] .screen-loading-monitor::after,
32
+ #note[data-state="blocked"] .screen-loading-art::before, #note[data-state="blocked"] .screen-loading-monitor::after,
33
+ #note[data-state="paused"] .screen-loading-art::before, #note[data-state="paused"] .screen-loading-monitor::after { animation: none; opacity: .3; }
34
+ @keyframes screen-scan { to { transform: translateX(340%); } }
35
+ @keyframes screen-breathe { 50% { opacity: .4; transform: scale(.92); } }
36
+ @media (prefers-reduced-motion: reduce) { .screen-loading-art::before, .screen-loading-monitor::after { animation: none; } }
37
+ @media (max-height: 420px) { #note { padding: 16px 72px; } .screen-loading-art { height: 64px; margin-bottom: 12px; }
38
+ .screen-loading-steps { margin-top: 14px; } #screen-loading-retry { margin-top: 12px; } }
39
+ `;
40
+ export const SCREEN_LOADING_HTML = String.raw `
41
+ <div id="note" data-state="connecting">
42
+ <div class="screen-loading-card">
43
+ <div class="screen-loading-art" aria-hidden="true"><div class="screen-loading-monitor"></div><div class="screen-loading-stand"></div></div>
44
+ <div role="status" aria-live="polite" aria-atomic="true">
45
+ <h2 id="screen-loading-title">正在连接电脑</h2>
46
+ <p id="screen-loading-detail">正在建立屏幕连接,画面会自动显示。</p>
47
+ </div>
48
+ <div class="screen-loading-steps" aria-hidden="true"><span id="screen-step-connect" class="current">连接电脑</span><i></i><span id="screen-step-picture">接收画面</span><i></i><span>显示桌面</span></div>
49
+ <button id="screen-loading-retry" class="gone" type="button">重新连接</button>
50
+ </div>
51
+ </div>`;
52
+ export const SCREEN_LOADING_SCRIPT = String.raw `
53
+ function createScreenLoading(root, retry, report) {
54
+ const title = root.querySelector('#screen-loading-title');
55
+ const detail = root.querySelector('#screen-loading-detail');
56
+ const button = root.querySelector('#screen-loading-retry');
57
+ const connectStep = root.querySelector('#screen-step-connect');
58
+ const pictureStep = root.querySelector('#screen-step-picture');
59
+ let phase = '', timer = 0, slow = false, disposed = false;
60
+ const pending = () => phase === 'connecting' || phase === 'receiving' || phase === 'recovering' || phase === 'adjusting';
61
+ const description = () => slow ? '这次连接耗时较长。可以继续等待,或重新连接。' :
62
+ phase === 'receiving' ? '电脑已响应,画面准备好后会自动显示。' :
63
+ phase === 'adjusting' ? '正在应用屏幕设置,请稍候。' :
64
+ phase === 'recovering' ? '正在尝试恢复画面,请稍候。' : '正在建立屏幕连接,画面会自动显示。';
65
+ function set(next, message) {
66
+ if (disposed || (phase === next && !message)) return;
67
+ const wasPending = pending();
68
+ phase = next;
69
+ root.dataset.state = phase;
70
+ root.classList.toggle('gone', phase === 'ready');
71
+ if (!pending()) { clearTimeout(timer); timer = 0; slow = false; }
72
+ else if (!wasPending) {
73
+ timer = setTimeout(() => {
74
+ if (!pending() || disposed) return;
75
+ slow = true;
76
+ detail.textContent = description();
77
+ button.classList.remove('gone');
78
+ }, 12000);
79
+ }
80
+ title.textContent = phase === 'connecting' ? '正在连接电脑' : phase === 'receiving' ? '正在接收画面' :
81
+ phase === 'recovering' ? '正在恢复画面' : phase === 'adjusting' ? '正在调整屏幕' : phase === 'paused' ? '屏幕已暂停' : phase === 'blocked' ? '画面已就绪,轻点开始' : phase === 'error' ? '暂时无法显示屏幕' : '画面已连接';
82
+ detail.textContent = message || (pending() ? description() : phase === 'paused' ? '返回屏幕后会自动继续。' : '');
83
+ button.classList.toggle('gone', phase !== 'error' && !slow);
84
+ connectStep.className = phase === 'connecting' ? 'current' : 'complete';
85
+ pictureStep.className = phase === 'receiving' || phase === 'recovering' ? 'current' : '';
86
+ report(phase);
87
+ }
88
+ button.addEventListener('click', (event) => { event.stopPropagation(); retry(); });
89
+ set('connecting');
90
+ return { set, get phase() { return phase; }, dispose() { disposed = true; clearTimeout(timer); } };
91
+ }
92
+ `;
93
+ //# sourceMappingURL=screen-loading.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screen-loading.js","sourceRoot":"","sources":["../../../../host/src/screen-loading.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqC7C,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;OAWtC,CAAC;AAER,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwC9C,CAAC"}
@@ -17,6 +17,7 @@
17
17
  // device can reach each other, it is a video track from the app, with the hands and the pointer on
18
18
  // data channels beside it; the page draws the pointer, which that picture does not have in it.
19
19
  // Otherwise it is H.264 down the page's socket, decoded here onto a canvas, as it always was.
20
+ import { SCREEN_LOADING_HTML, SCREEN_LOADING_SCRIPT, SCREEN_LOADING_STYLE } from "./screen-loading.js";
20
21
  const icon = (body) => `<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
21
22
  const ICONS = {
22
23
  view: icon('<path d="M2.5 12s3.5-6.5 9.5-6.5 9.5 6.5 9.5 6.5-3.5 6.5-9.5 6.5S2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.8"/>'),
@@ -52,8 +53,6 @@ const STYLE = String.raw `
52
53
  #pointer.pressed { background: rgba(255,255,255,0.4); }
53
54
  #pointer.own { border: 0; border-radius: 0; box-shadow: none; background: transparent center / 100% 100% no-repeat; }
54
55
  #stage.own { cursor: none; }
55
- #note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 32px;
56
- color: rgba(255,255,255,0.72); text-align: center; pointer-events: none; }
57
56
  .glass { background: rgba(38,38,42,0.74); -webkit-backdrop-filter: blur(22px) saturate(1.6); backdrop-filter: blur(22px) saturate(1.6);
58
57
  border: 0.5px solid rgba(255,255,255,0.16); box-shadow: 0 8px 28px rgba(0,0,0,0.38); }
59
58
  #control-shell { position: absolute; pointer-events: none; z-index: 1; will-change: width, height, transform, border-radius, opacity; }
@@ -597,8 +596,22 @@ const mods = new Set();
597
596
 
598
597
  // ---- The picture ----------------------------------------------------------
599
598
 
600
- let sessionError = "";
601
- const say = (text) => { text = sessionError || text; note.textContent = text; note.classList.toggle("gone", !text); };
599
+ let sessionError = "", sessionPhase = "", hasPicture = false;
600
+ const loading = createScreenLoading(note, () => {
601
+ if (app && chrome.canRetryScreen) tellApp({ type: "screenRetry" });
602
+ else if (typeof window.__linkshellReload === "function") window.__linkshellReload({});
603
+ else location.reload();
604
+ }, (phase) => window.__linkshellScreenState?.(phase));
605
+ addEventListener("pagehide", () => loading.dispose(), { once: true });
606
+ const say = (text) => {
607
+ if (!text) hasPicture = true;
608
+ if (sessionError) loading.set("error", sessionError);
609
+ else if (text) loading.set("error", text);
610
+ else if (sessionPhase) loading.set(sessionPhase);
611
+ else loading.set("ready");
612
+ };
613
+ // Transport updates must not hide a configuration failure or a paused retained session.
614
+ const showProgress = (phase) => { if (!sessionError && !sessionPhase) loading.set(phase); };
602
615
  const round = (value, digits) => (value == null || !isFinite(value) ? null : +value.toFixed(digits === undefined ? 1 : digits));
603
616
  const hex = (n) => n.toString(16).padStart(2, "0");
604
617
  function sps(unit) {
@@ -694,11 +707,13 @@ const session = createScreenSession({ active: true, display: Number(query.get("d
694
707
  changed: (state) => {
695
708
  width = state.width;
696
709
  const previousError = sessionError;
710
+ const previousPhase = sessionPhase;
697
711
  sessionError = state.error;
712
+ sessionPhase = state.paused ? "paused" : !state.inputReady ? "adjusting" : "";
698
713
  if (sessionError) say(sessionError);
699
- else if (state.paused) say("屏幕已暂停");
700
- else if (!state.inputReady) say("正在调整屏幕…");
701
- else if (previousError || note.textContent === "屏幕已暂停" || note.textContent === "正在调整屏幕…") say("");
714
+ else if (sessionPhase) loading.set(sessionPhase);
715
+ // Configuration acknowledgement enables input; only received media proves the first picture exists.
716
+ else if (previousError || previousPhase) loading.set(hasPicture ? "ready" : "receiving");
702
717
  if (state.inputReady && mode !== "view") askControl();
703
718
  refreshWidths();
704
719
  refresh();
@@ -725,7 +740,7 @@ function waitForCapabilities() {
725
740
  if (!session.known()) session.receive({ t: "capabilities", version: 1, configure: false, pause: false, retainMs: 0 });
726
741
  }, 10000);
727
742
  }
728
- ws.onopen = () => { session.open(); waitForCapabilities(); if (mode !== "view") askControl(); };
743
+ ws.onopen = () => { if (loading.phase === "connecting") showProgress("receiving"); session.open(); waitForCapabilities(); if (mode !== "view") askControl(); };
729
744
  // Out of sight, a page answers late (its timers slow to one a second) and shows nothing: the host sends no
730
745
  // picture down the socket meanwhile, and starts again at a keyframe.
731
746
  document.addEventListener("visibilitychange", () => session.visibility(document.hidden));
@@ -803,7 +818,7 @@ ws.onclose = () => {
803
818
  drop();
804
819
  if (requestRelay()) return;
805
820
  tellApp({ type: "screenClosed" });
806
- if (blocked || note.classList.contains("gone") || note.textContent.startsWith("正在")) say("屏幕连接已断开");
821
+ if (loading.phase !== "error") say("屏幕连接已断开,请确认电脑在线后重新连接。");
807
822
  blocked = false;
808
823
  };
809
824
 
@@ -936,15 +951,17 @@ function watch(mine) {
936
951
  const state = mine.pc.connectionState || mine.pc.iceConnectionState;
937
952
  if (state === "failed" || mine.pc.iceConnectionState === "failed") return giveUp(mine.up ? "the connection failed" : "no path between the two (ICE failed)");
938
953
  if (state === "disconnected") {
954
+ showProgress("recovering");
939
955
  // A path that drops for a moment comes back by itself; one gone for longer is given up.
940
956
  if (mine.up && !mine.away) mine.away = setTimeout(() => away(mine, performance.now()), 4000);
941
957
  } else if (state === "connected" || state === "completed") {
958
+ if (loading.phase !== "ready") showProgress("receiving");
942
959
  clearTimeout(mine.away);
943
960
  mine.away = 0;
944
961
  if (mine.up) return;
945
962
  mine.up = true;
946
963
  clearTimeout(patience);
947
- if (!live) patience = setTimeout(frameless, 4000);
964
+ if (loading.phase !== "ready") patience = setTimeout(frameless, 4000);
948
965
  }
949
966
  }
950
967
 
@@ -973,6 +990,7 @@ function frameless() {
973
990
  function giveUp(reason) {
974
991
  if (gaveUp) return;
975
992
  gaveUp = true;
993
+ showProgress("recovering");
976
994
  const pc = rtc && rtc.pc;
977
995
  const told = (detail) => {
978
996
  drop();
@@ -989,7 +1007,7 @@ function requestRelay() {
989
1007
  if (!relayRequested) {
990
1008
  relayRequested = true;
991
1009
  tellApp({ type: "screenFallback" });
992
- say("正在连接电脑屏幕…");
1010
+ showProgress("recovering");
993
1011
  }
994
1012
  return true;
995
1013
  }
@@ -1010,7 +1028,7 @@ function fallBack() {
1010
1028
  gaveUp = true;
1011
1029
  drop();
1012
1030
  if (requestRelay()) return;
1013
- if (blocked) say("正在连接电脑屏幕…");
1031
+ showProgress("recovering");
1014
1032
  blocked = false;
1015
1033
  if (!windowsHost && !("VideoDecoder" in window)) return unseeable();
1016
1034
  if (tried) hint(live ? "直连断开了:已改用兼容方式传画面,延迟会高一些" : "没能和电脑直连:已改用兼容方式传画面,延迟会高一些", 5000);
@@ -1024,12 +1042,12 @@ function play() {
1024
1042
  started.then(() => {
1025
1043
  if (!blocked) return;
1026
1044
  blocked = false;
1027
- say(live ? "" : "正在连接电脑屏幕…");
1045
+ showProgress("receiving");
1028
1046
  }, (error) => {
1029
1047
  // A new stream replacing the one being started rejects the first call too; that is not a refusal.
1030
1048
  if (error.name === "AbortError" || !rtc) return;
1031
1049
  blocked = true;
1032
- say("轻点屏幕,开始显示画面");
1050
+ if (!sessionError && !sessionPhase) loading.set("blocked", "浏览器需要你的确认,轻点屏幕即可开始观看。");
1033
1051
  });
1034
1052
  }
1035
1053
  // What a browser takes as someone's say-so to start a video.
@@ -1041,19 +1059,34 @@ function sized() {
1041
1059
  // While this page presents the frames itself, the size that counts is the frame's own: the
1042
1060
  // <video> is kept running as the fallback but is not what is on screen.
1043
1061
  if (presenter) {
1044
- clearTimeout(patience);
1062
+ if (presenter.shown) clearTimeout(patience);
1045
1063
  if (!live) show(true);
1046
1064
  return;
1047
1065
  }
1048
1066
  content.w = video.videoWidth;
1049
1067
  content.h = video.videoHeight;
1050
- if (!blocked) say("");
1051
1068
  if (live) return layout();
1052
- clearTimeout(patience);
1053
1069
  show(true);
1054
1070
  }
1055
1071
  video.addEventListener("resize", sized);
1056
1072
  video.addEventListener("loadedmetadata", sized);
1073
+ // Metadata and a connected socket do not mean a frame is visible yet.
1074
+ function videoShown() {
1075
+ if (!rtc || blocked || video.readyState < 2 || presenter) return;
1076
+ clearTimeout(patience);
1077
+ sized();
1078
+ say("");
1079
+ }
1080
+ if ("requestVideoFrameCallback" in video) {
1081
+ const nextFrame = () => {
1082
+ if (loading.phase !== "ready") videoShown();
1083
+ video.requestVideoFrameCallback(nextFrame);
1084
+ };
1085
+ video.requestVideoFrameCallback(nextFrame);
1086
+ } else {
1087
+ video.addEventListener("playing", videoShown);
1088
+ video.addEventListener("timeupdate", videoShown);
1089
+ }
1057
1090
 
1058
1091
  // ---- Presenting the track's frames this page's own way --------------------
1059
1092
 
@@ -1129,6 +1162,7 @@ async function presentTrack(track) {
1129
1162
  draws.end();
1130
1163
  device.queue.submit([pass.finish()]);
1131
1164
  mine.shown += 1;
1165
+ say("");
1132
1166
  if (mine.w !== frame.displayWidth || mine.h !== frame.displayHeight) {
1133
1167
  mine.w = frame.displayWidth;
1134
1168
  mine.h = frame.displayHeight;
@@ -1138,7 +1172,6 @@ async function presentTrack(track) {
1138
1172
  // the <video> beside it was allowed to start playing.
1139
1173
  clearTimeout(patience);
1140
1174
  blocked = false;
1141
- say("");
1142
1175
  if (!live) show(true);
1143
1176
  else layout();
1144
1177
  }
@@ -3108,7 +3141,7 @@ if (nativePicture) {
3108
3141
  session.reconnect();
3109
3142
  waitForCapabilities();
3110
3143
  Object.assign(control, { asked: false, known: false, trusted: false });
3111
- say("正在连接电脑屏幕…");
3144
+ showProgress("connecting");
3112
3145
  if (mode !== "view") askControl();
3113
3146
  },
3114
3147
  };
@@ -3126,16 +3159,16 @@ if (nativePicture) {
3126
3159
  }
3127
3160
  tellApp({ type: "ready", mode });
3128
3161
  `;
3129
- const SCRIPT = `(() => {${VIEWER_LOGIC}${BODY}})();`;
3162
+ const SCRIPT = `(() => {${VIEWER_LOGIC}${SCREEN_LOADING_SCRIPT}${BODY}})();`;
3130
3163
  export function viewerPage(platform = "darwin") {
3131
3164
  return `<!doctype html>
3132
3165
  <html><head><meta charset="utf-8">
3133
3166
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
3134
3167
  <title>屏幕</title>
3135
- <style>${STYLE}</style></head>
3168
+ <style>${STYLE}${SCREEN_LOADING_STYLE}</style></head>
3136
3169
  <body data-platform="${platform === "win32" ? "win32" : "darwin"}">
3137
3170
  <div id="stage"><div id="picture"><canvas id="screen"></canvas><canvas id="gpu" class="gone"></canvas><video id="video" class="gone" autoplay playsinline muted></video><div id="pointer"></div></div></div>
3138
- <div id="note">正在连接电脑屏幕…</div>
3171
+ ${SCREEN_LOADING_HTML}
3139
3172
  <div id="toast" class="glass"></div>
3140
3173
  <div id="menu" class="glass gone">
3141
3174
  <div class="choice" role="button" aria-label="只看,不操作电脑" data-mode="view"><i></i><div><b>只看</b><span>不操作电脑</span></div></div>
@@ -1 +1 @@
1
- {"version":3,"file":"screen-viewer.js","sourceRoot":"","sources":["../../../../host/src/screen-viewer.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,2EAA2E;AAC3E,6EAA6E;AAC7E,mEAAmE;AACnE,EAAE;AACF,4DAA4D;AAC5D,yEAAyE;AACzE,2CAA2C;AAC3C,6EAA6E;AAC7E,+EAA+E;AAC/E,2CAA2C;AAC3C,qGAAqG;AACrG,+EAA+E;AAC/E,6FAA6F;AAC7F,EAAE;AACF,oGAAoG;AACpG,mGAAmG;AACnG,+FAA+F;AAC/F,8FAA8F;AAE9F,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAC5B,uJAAuJ,IAAI,QAAQ,CAAC;AAEtK,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,IAAI,CAAC,qHAAqH,CAAC;IACjI,QAAQ,EAAE,IAAI,CAAC,6DAA6D,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,oGAAoG,CAAC;IACjH,QAAQ,EAAE,IAAI,CAAC,iJAAiJ,CAAC;IACjK,GAAG,EAAE,IAAI,CAAC,0EAA0E,CAAC;IACrF,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,IAAI,EAAE,IAAI,CAAC,4BAA4B,CAAC;IACxC,KAAK,EAAE,IAAI,CAAC,qDAAqD,CAAC;IAClE,OAAO,EAAE,IAAI,CAAC,sEAAsE,CAAC;IACrF,IAAI,EAAE,IAAI,CAAC,iEAAiE,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,4CAA4C,CAAC;CAC1D,CAAC;AAEF,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoHvB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0YrC,CAAC;AAEF,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAihFtB,CAAC;AAEF,MAAM,MAAM,GAAG,WAAW,YAAY,GAAG,IAAI,OAAO,CAAC;AAErD,MAAM,UAAU,UAAU,CAAC,WAA4B,QAAQ;IAC7D,OAAO;;;;SAIA,KAAK;uBACS,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;;;;;;;;;;iHAUiD,KAAK,CAAC,KAAK;;;;;;;;;;;;;;;kEAe1D,KAAK,CAAC,QAAQ;iEACf,KAAK,CAAC,KAAK;+DACb,KAAK,CAAC,GAAG;gEACR,KAAK,CAAC,IAAI;;;;;;;;;;;;;;;4CAe9B,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,aAAa,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;;4CAEtF,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,aAAa,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;;;;;;;;;;;;;;;;;;;;;;;oCAuBnG,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG;oCAClC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG;;;;;;;;;;6CAUzB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;UACxD,MAAM;eACD,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"screen-viewer.js","sourceRoot":"","sources":["../../../../host/src/screen-viewer.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,2EAA2E;AAC3E,6EAA6E;AAC7E,mEAAmE;AACnE,EAAE;AACF,4DAA4D;AAC5D,yEAAyE;AACzE,2CAA2C;AAC3C,6EAA6E;AAC7E,+EAA+E;AAC/E,2CAA2C;AAC3C,qGAAqG;AACrG,+EAA+E;AAC/E,6FAA6F;AAC7F,EAAE;AACF,oGAAoG;AACpG,mGAAmG;AACnG,+FAA+F;AAC/F,8FAA8F;AAE9F,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEvG,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAC5B,uJAAuJ,IAAI,QAAQ,CAAC;AAEtK,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,IAAI,CAAC,qHAAqH,CAAC;IACjI,QAAQ,EAAE,IAAI,CAAC,6DAA6D,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,oGAAoG,CAAC;IACjH,QAAQ,EAAE,IAAI,CAAC,iJAAiJ,CAAC;IACjK,GAAG,EAAE,IAAI,CAAC,0EAA0E,CAAC;IACrF,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,MAAM,EAAE,IAAI,CAAC,oDAAoD,CAAC;IAClE,IAAI,EAAE,IAAI,CAAC,4BAA4B,CAAC;IACxC,KAAK,EAAE,IAAI,CAAC,qDAAqD,CAAC;IAClE,OAAO,EAAE,IAAI,CAAC,sEAAsE,CAAC;IACrF,IAAI,EAAE,IAAI,CAAC,iEAAiE,CAAC;IAC7E,KAAK,EAAE,IAAI,CAAC,4CAA4C,CAAC;CAC1D,CAAC;AAEF,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkHvB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0YrC,CAAC;AAEF,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmjFtB,CAAC;AAEF,MAAM,MAAM,GAAG,WAAW,YAAY,GAAG,qBAAqB,GAAG,IAAI,OAAO,CAAC;AAE7E,MAAM,UAAU,UAAU,CAAC,WAA4B,QAAQ;IAC7D,OAAO;;;;SAIA,KAAK,GAAG,oBAAoB;uBACd,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;;EAE9D,mBAAmB;;;;;;;;iHAQ4F,KAAK,CAAC,KAAK;;;;;;;;;;;;;;;kEAe1D,KAAK,CAAC,QAAQ;iEACf,KAAK,CAAC,KAAK;+DACb,KAAK,CAAC,GAAG;gEACR,KAAK,CAAC,IAAI;;;;;;;;;;;;;;;4CAe9B,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,aAAa,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;;4CAEtF,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,aAAa,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;;;;;;;;;;;;;;;;;;;;;;;oCAuBnG,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG;oCAClC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG;;;;;;;;;;6CAUzB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;UACxD,MAAM;eACD,CAAC;AAChB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkshell-cli",
3
- "version": "0.10.49",
3
+ "version": "0.10.51",
4
4
  "description": "Follow, steer and approve the coding agents on your computer (Claude Code, Codex, Gemini, Copilot and more) from your phone",
5
5
  "type": "module",
6
6
  "main": "dist/cli/src/index.js",
@@ -34,9 +34,9 @@
34
34
  "node-pty": "^1.1.0",
35
35
  "qrcode-terminal": "^0.12.0",
36
36
  "ws": "^8.21.0",
37
- "@linkshell/gateway": "0.6.26",
38
- "@linkshell/host": "0.4.47",
39
- "@linkshell/wire": "0.2.17"
37
+ "@linkshell/gateway": "0.6.28",
38
+ "@linkshell/wire": "0.2.17",
39
+ "@linkshell/host": "0.4.49"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/ws": "^8.5.14",
@@ -6,6 +6,7 @@ import { homedir } from "node:os";
6
6
  import type { Readable } from "node:stream";
7
7
  import type { Socket } from "node:net";
8
8
  import WebSocket, { WebSocketServer } from "ws";
9
+ import { localWebSocket } from "@linkshell/host/local-transport";
9
10
  import { isMethodName, machineInfoSchema, rpcMessageSchema, type MachineEntry } from "@linkshell/wire";
10
11
  import { getValidToken, loadAuth } from "../auth.js";
11
12
  import { runLogin } from "./login.js";
@@ -85,7 +86,7 @@ async function loadAssets(directory: string): Promise<Map<string, Buffer>> {
85
86
  async function localComputer(target: string, upstreams: Set<WebSocket>): Promise<LocalComputer | undefined> {
86
87
  // Query the existing socket directly: discovering this computer never starts a host.
87
88
  return new Promise((resolve) => {
88
- const client = new WebSocket(`ws+unix://${target}:/`, { perMessageDeflate: false, handshakeTimeout: 1_500, maxPayload: MAX_PROXY_BYTES });
89
+ const client = localWebSocket(target, 1_500, { maxPayload: MAX_PROXY_BYTES });
89
90
  upstreams.add(client);
90
91
  const finish = (computer?: LocalComputer) => {
91
92
  clearTimeout(timeout);
@@ -107,7 +108,7 @@ async function localComputer(target: string, upstreams: Set<WebSocket>): Promise
107
108
  key: "desktop:local", gateway: "desktop:local",
108
109
  machine: {
109
110
  id: info.machineId, name: info.hostname.replace(/\.local$/, ""), platform: info.platform,
110
- // Local transport uses a protected Unix socket, not a relay identity or tunnel.
111
+ // Local transport uses the host's Unix socket or Windows named pipe.
111
112
  signKey: "", boxKey: "", online: true, via: "paired",
112
113
  },
113
114
  });
@@ -127,9 +128,7 @@ async function account(signal: AbortSignal) {
127
128
  }
128
129
 
129
130
  function proxyHost(client: WebSocket, target: string, upstreams: Set<WebSocket>): void {
130
- const host = new WebSocket(`ws+unix://${target}:/`, {
131
- perMessageDeflate: false, handshakeTimeout: 3_000, maxPayload: MAX_PROXY_BYTES,
132
- });
131
+ const host = localWebSocket(target, 3_000, { maxPayload: MAX_PROXY_BYTES });
133
132
  upstreams.add(host);
134
133
  let pending: string[] = [];
135
134
  let pendingBytes = 0;
@@ -271,7 +270,7 @@ export async function startDesktopWorkspace(options: { assets: string; home?: st
271
270
  }
272
271
  if (!["GET", "HEAD"].includes(request.method ?? "")) throw new HttpError(405, "不支持此请求方法");
273
272
  const assetPath = path === "/" ? "/index.html" : path;
274
- const content = path === "/config.js" ? Buffer.from(`window.__LINKSHELL_CONFIG__ = ${JSON.stringify({ desktop: true, deployment: "official", previewOrigin })};\n`) : assets.get(assetPath);
273
+ const content = path === "/config.js" ? Buffer.from(`window.__LINKSHELL_CONFIG__ = ${JSON.stringify({ desktop: true, desktopPlatform: process.platform, deployment: "official", previewOrigin })};\n`) : assets.get(assetPath);
275
274
  if (!content) throw new HttpError(404, "未找到资源");
276
275
  response.writeHead(200, {
277
276
  "content-type": MIME[extname(assetPath)] ?? "application/octet-stream", "content-length": content.byteLength,